| Title: | Data Profiling and Quality Measurement |
|---|---|
| Description: | Provides general-purpose data profiling and quality analysis for tabular data. Discovers structural patterns, implicit types, mixed date formats and reusable findings about potential data issues. Defines quality frameworks with user-declared dimensions and factors, reusable metrics with explicit granularity, typed aggregation, evaluation profiles, versioned histories, structural drift detection, and self-contained HyperText Markup Language reports without external rendering software. Produces editable cleaning plans and applies only explicitly selected actions while retaining an audit log. Includes bounded value distributions, quantiles, mixed-type associations, temporal regularity checks, and confirmable measurement-scale proposals. The general model follows the data-quality lineage described by Batini and Scannapieco (2016) <doi:10.1007/978-3-319-24106-7>. The package ships a traceable implementation of the Data Quality Management Framework for Digital Government, version 1.6, published by the Agency for Electronic Government and the Information and Knowledge Society (AGESIC) (2020) <https://www.gub.uy/agencia-gobierno-electronico-sociedad-informacion-conocimiento/>, and an optional taxonomy of the fifteen characteristics in the International Organization for Standardization and International Electrotechnical Commission standard ISO/IEC 25012:2008 <https://www.iso.org/standard/35736.html>. It also includes the four levels and nineteen principles of the United Nations national quality assurance framework, adapted for Latin America and the Caribbean by the Statistical Conference of the Americas (CEA) of the Economic Commission for Latin America and the Caribbean (CEPAL), based on the United Nations manual (2019) <https://unstats.un.org/unsd/methodology/dataquality/> and the regional guide (2022) <https://repositorio.cepal.org/handle/11362/47464>. |
| Authors: | Sebastián Lucas [aut, cre] (ORCID: <https://orcid.org/0009-0009-9068-0276>), Robyn Speer [cph] (encoding-repair design and character tables, from ftfy 6.3.1), Nicholas Tierney [cph] (missing-value sentinel vectors, from naniar 1.1.0), Di Cook [cph] (missing-value sentinel vectors, from naniar 1.1.0), Miles McBain [cph] (missing-value sentinel vectors, from naniar 1.1.0), Colin Fay [cph] (missing-value sentinel vectors, from naniar 1.1.0) |
| Maintainer: | Sebastián Lucas <[email protected]> |
| License: | GPL-3 |
| Version: | 0.1.0 |
| Built: | 2026-10-20 10:23:30 UTC |
| Source: | https://github.com/sebollin/lupa |
perfilar(), perfilar_dbi() y perfilar_coleccion() devuelven objetos con
formas distintas, y eso convertía la lectura en un acertijo:
perfil$general$filas funciona sobre la salida en memoria y devuelve NULL
sobre la salida DBI, donde el conteo vive en resumen_tabla$meta$filas. Un
NULL silencioso en un guion de medición es la peor forma de fallar: no
avisa, y lo que sigue calcula sobre nada.
hallazgos(x, ...) columnas(x, ...) cobertura(x, ...) n_filas(x, ...) sql_perfil(x, ...)hallazgos(x, ...) columnas(x, ...) cobertura(x, ...) n_filas(x, ...) sql_perfil(x, ...)
x |
Objeto de |
... |
Sin uso, para compatibilidad de métodos. |
Estos accesores no agregan información: le ponen un solo nombre a la que ya está. La forma concreta de cada objeto sigue disponible y documentada.
Lo que no hay, no se inventa. Un perfilar_dbi() sin muestra leída no
tiene hallazgos por fila, y hallazgos() devuelve una tabla vacía con su
aviso, no una tabla que aparente que se midió y no había nada.
hallazgos() devuelve un data.frame con una fila por hallazgo;
columnas(), un data.frame con una fila por columna de la tabla
perfilada y sus métricas y diagnósticos; cobertura() devuelve la tabla de
diagnósticos no evaluados —y, sobre un perfil_dbi, la de métricas
SQL, que es la cobertura de ese objeto: sus diagnósticos no evaluados
viajan en x$perfil_muestra$cobertura_diagnosticos, con el esquema de
perfilar(), y la impresión del perfil los declara—; n_filas() devuelve el conteo de filas del
alcance, o un vector con un conteo por tabla para una colección. Devuelve
NA con su motivo cuando el objeto no conoce el conteo. El significado de
cada campo de columnas() se explica por familias en los detalles de
perfilar(). sql_perfil() devuelve la tabla de consultas emitidas;
sobre una colección incluye la identidad de la tabla y conserva las
consultas aunque no se retengan los perfiles completos.
perfilar(), perfilar_dbi(), perfilar_coleccion()
perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) nrow(hallazgos(perfil)) n_filas(perfil) nrow(cobertura(perfil))perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) nrow(hallazgos(perfil)) n_filas(perfil) nrow(cobertura(perfil))
Aplica exactamente una de las cuatro agregaciones del marco: ratio,
ratio_umbral, promedio o promedio_ponderado. La transición se valida
contra el grafo de transiciones_granularidad().
agregar( medidas, destino, funcion = c("ratio", "ratio_umbral", "promedio", "promedio_ponderado"), umbral = NULL, pesos = NULL, coleccion = NULL, colecciones = NULL, organizacion = NULL, organizaciones = NULL )agregar( medidas, destino, funcion = c("ratio", "ratio_umbral", "promedio", "promedio_ponderado"), umbral = NULL, pesos = NULL, coleccion = NULL, colecciones = NULL, organizacion = NULL, organizaciones = NULL )
medidas |
Data frame producido por |
destino |
Granularidad de destino. |
funcion |
Una de |
umbral |
Umbral en |
pesos |
Vector numérico requerido por |
coleccion |
Frontera declarada, exigida cuando |
colecciones |
Lista nombrada de objetos de |
organizacion |
Frontera institucional declarada con |
organizaciones |
Lista de objetos de Los dos niveles institucionales son opcionales: un análisis de calidad
no siempre tiene una organización detrás —una entrega suelta, un archivo
que alguien mandó, una base sin dueño declarado—, y nada obliga a pasar por
ellos. Existen para quien los necesita, y sin declaración |
ratio sólo acepta medidas booleanas. ratio_umbral sólo acepta medidas
reales. Los promedios aceptan esos dos tipos y ningún otro, y siempre
producen resultado real en [0, 1]. Los tres tipos no acotados
—numero_real, entero y duracion— se rechazan nombrando la métrica que
los declara: el promedio de una duración en días es un número de días, y
publicarlo como resultado real en [0, 1] lo presentaría como una
proporción. La guarda es por tipo declarado, no por el valor observado:
antes una duración de 0,25 y 0,75 días pasaba y la misma métrica con
1,5 días abortaba, así que el mismo modelo cambiaba de conducta según los
datos que le tocaran. Para el promedio ponderado, los pesos deben estar en
[0, 1] y sumar uno dentro de cada objeto de destino. La columna
orientacion se conserva sin invertir el resultado: un ratio de una métrica
de defecto sigue siendo la proporción de defectos.
Objeto medicion agregado, con una fila por objeto de destino.
Una agregación hereda lo que sus partes declararon: configuracion_modelo,
configuracion_aplicabilidad, cobertura_metricas, marco_calidad y
fecha_declarada viajan en el objeto. Lo último no es decorativo: sin
fecha_declarada, la guarda que impide comparar dos entregas en orden
invertido queda desactivada y comparar_evaluaciones() publicaría el delta
con el signo al revés.
alcance_medidas —«midió tres celdas de cuatro»— también viaja, pero
reexpresado en la clave del agregado: la tabla que publica medir() está
indexada por metrica_instanciada ([email protected]) y el agregado renombra la
métrica (agregada:ratio:Formato), así que arrastrarla tal cual dejaba una
declaración que no se podía atribuir a ninguna fila. Los conteos se suman
por objeto de destino, y sólo cuando todas las partes declaran la misma
unidad; si no, la fila declara la mezcla en lugar de publicar un total que no
estaría en ninguna unidad. Las partes completas también suman —una
columna de 6 de 6 junto a una de 5 de 6 da 11 de 12—, también cuando se
agregaron por separado y se unieron con rbind(); y en un destino por
fila (instanciaEntidad) cada fila cuenta sus propias celdas: se declaran
sólo las incompletas, con su número de fila en el motivo.
Una medida repetida —el mismo id_medida dos veces, como deja una medición
unida consigo misma— se rechaza, como en evaluar(). El id_medida de cada
fila agregada nombra lo que agrega —destino, función, métrica y objeto, como
[email protected]—, así que dos partes agregadas por separado
se pueden unir y subir de nivel, y el mismo agregado repetido sigue
repitiendo su identificador. Cada nombre lleva escapados los signos que lo
separan del siguiente —el punto, la coma—, y desde la colección para arriba
el identificador nombra también las partes que reúne: dos conjuntos
distintos, o dos colecciones sin nombre, no comparten identificador. Un valor que no se
puede agregar se rechaza nombrando la métrica y la causa: un tipo no
acotado, una medida ausente o suprimida, o un valor fuera de [0, 1].
umbral sólo se acepta con ratio_umbral, y el borde se compara con
tolerancia de redondeo: 1 - 0.9 alcanza el umbral 0.1.
Las coberturas de frontera (cobertura_coleccion y sus hermanas) no se
arrastran: cada agregación calcula la de su propio destino con las partes que
entraron a ese número, y hereda las de sus partes en cobertura_de_partes.
Dos partes agregadas por separado y unidas con rbind() conservan cada una
la suya, en cualquier orden de la unión, y el tablero, el índice, el
informe, evaluar() e historico_calidad() las publican todas. Unidas
dos corridas distintas, cada cobertura lleva además su id_medicion, y el
histórico registra la parte no medida en la corrida a la que le faltó. Una
colección lleva en cobertura_metricas sólo la de sus propias tablas.
Cuando hay pesos, el objeto los publica en pesos_declarados, con el nombre
de la parte que recibió cada uno, de modo que el número se pueda rehacer con
lo que el objeto trae. Y si alguna parte entró con peso cero, se declara en
partes_con_peso_cero: entró al número sin aportarle nada, queda contada en
la identidad de la fila y leerlo sin saberlo es leer otra cosa. La
declaración depende de que haya un peso cero, no del destino: antes sólo
existía en los cuatro destinos que tienen cobertura de frontera, y en
conjuntoEntidades el mismo peso cero pasaba en silencio.
Cuando el destino es conjuntoEntidades, promedio combina las partes sin
pesos porque las medidas de nivel entidad no llevan su cantidad de filas.
El resultado lo declara en advertencia_agregacion: es un promedio sin pesos
y agregar() no conoce el alcance de cada parte. En coleccion, la frontera
se declara y por eso sólo se admite promedio_ponderado: sin pesos no se
puede decidir cuanto debe aportar cada tabla de esa coleccion.
No existe una transición hacia factor, dimensión o modelo: esos campos son taxonómicos y esta función no calcula un índice global.
nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo) instancia <- instanciar(especifica, "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) agregar(medidas, "atributo", "ratio")nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo) instancia <- instanciar(especifica, "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) agregar(medidas, "atributo", "ratio")
Es la puerta de entrada al recorrido de lupa. Construye el perfil, las
distribuciones, asociaciones, diagnóstico temporal, clasificación confirmable
de variables, propuesta de modelo, medición agregada, tablero, cobertura
conceptual y plan de limpieza. No modifica los datos.
analizar( datos, nombre = .nombre_de_los_datos(substitute(datos)), fecha = Sys.time(), argumentos_perfil = list(), metadatos_variables = NULL, modelo_confirmado = NULL, propuesta_confirmada = NULL, marco = NULL, perfil_evaluacion = NULL, id_medicion = NULL, medir_propuesta = TRUE, conservar_detalle_medicion = FALSE, muestra = 1e+05, muestra_asociacion = 10000, max_valores = 20L, probabilidades = c(0, 0.25, 0.5, 0.75, 1), umbral_asociacion = 0.3, max_columnas_asociacion = 50L, max_niveles_asociacion = 50L, max_pares_asociacion = 500L, metodo_asociacion_numerica = c("pearson", "spearman"), calendario = 1:7, frecuencia_dias = NULL, max_huecos = 20L, max_columnas_temporales = 50L, conservar_datos = FALSE, proteger_datos_personales = TRUE, ... )analizar( datos, nombre = .nombre_de_los_datos(substitute(datos)), fecha = Sys.time(), argumentos_perfil = list(), metadatos_variables = NULL, modelo_confirmado = NULL, propuesta_confirmada = NULL, marco = NULL, perfil_evaluacion = NULL, id_medicion = NULL, medir_propuesta = TRUE, conservar_detalle_medicion = FALSE, muestra = 1e+05, muestra_asociacion = 10000, max_valores = 20L, probabilidades = c(0, 0.25, 0.5, 0.75, 1), umbral_asociacion = 0.3, max_columnas_asociacion = 50L, max_niveles_asociacion = 50L, max_pares_asociacion = 500L, metodo_asociacion_numerica = c("pearson", "spearman"), calendario = 1:7, frecuencia_dias = NULL, max_huecos = 20L, max_columnas_temporales = 50L, conservar_datos = FALSE, proteger_datos_personales = TRUE, ... )
datos |
Tabla que se desea analizar. |
nombre |
Nombre de la entrega. |
fecha |
Fecha y hora reproducible de la corrida. Se lee una vez, como
en |
argumentos_perfil |
Lista de argumentos adicionales para |
metadatos_variables |
Declaraciones para |
modelo_confirmado |
Modelo creado por |
propuesta_confirmada |
Propuesta editada por el usuario o |
marco |
Taxonomía opcional creada por |
perfil_evaluacion |
Perfil explícito para |
id_medicion |
Identificador opcional enviado a |
medir_propuesta |
Si se mide automáticamente la propuesta en estado
|
conservar_detalle_medicion |
Si se retienen las medidas fila a fila.
Es |
muestra |
Límite de filas para perfil, distribuciones y enumeración de niveles observados. |
muestra_asociacion |
Límite común de filas para asociaciones. |
max_valores |
Máximo de valores por tabla de frecuencias. |
probabilidades |
Cuantiles solicitados. |
umbral_asociacion |
Asociación mínima informada. |
max_columnas_asociacion |
Máximo de columnas para asociaciones. |
max_niveles_asociacion |
Máximo de niveles categóricos. |
max_pares_asociacion |
Máximo de pares devueltos. |
metodo_asociacion_numerica |
Medida entre columnas numéricas que usa
|
calendario |
Días ISO usados por el análisis temporal. |
frecuencia_dias |
Frecuencia temporal conocida o |
max_huecos |
Máximo de grupos de huecos por columna. |
max_columnas_temporales |
Máximo de columnas temporales analizadas. |
conservar_datos |
Si el objeto retiene una copia de la entrada. Es
|
proteger_datos_personales |
Si perfiles y resúmenes ocultan valores de columnas cuya clasificación activa protección automática, incluidos estadísticos de orden, cuantiles y rangos temporales. |
... |
Argumentos con nombre enviados a |
Por omisión mide todas las sugerencias de la propuesta cuyo estado es
"lista", aunque no estuvieran activas, y declara que esa selección fue
realizada por lupa sin confirmación. Agrega inmediatamente el detalle y
conserva tablero_calidad(); las medidas fila a fila sólo quedan en el
resultado si conservar_detalle_medicion = TRUE. La evaluación, cuando se
solicita, usa la medición agregada.
Los hallazgos casi_clave del perfil se reiteran en advertencias, con su
columna, valores en colisión, frecuencias y criterio observado, para que no
queden ocultos dentro del recorrido integral.
Objeto S3 analisis con todos los componentes y su cobertura.
Tres diferencias respecto de llamar a las funciones sueltas, y las tres son
deliberadas. La evaluación usa la medición agregada, no las medidas por
fila: evaluar(medir(...), perfil) aplica la regla a cada medida y puede
dar otra cifra que analizar(perfil_evaluacion = perfil) sobre los mismos
datos —una proporción de medidas contra una proporción de métricas—, y
sobre medidas de 0 o 1 los tres perfiles_madurez() no se distinguen.
Para reproducirla con las funciones sueltas, agregar() antes de
evaluar(). La entrada puede ser una matriz de dos dimensiones: se la pasa
a perfilar() sin convertir, para que sea él quien la convierta y lo
declare en meta$entrada_convertida. Y cuando una columna temporal
pertenece a una columna protegida, temporal$resumen gana la columna
proteccion_temporal con el texto [rangos y huecos protegidos]: la
protección se aplica y se declara en la misma tabla donde faltan los
valores. La llamada directa a analizar_tiempo() respeta la misma
declaración cuando recibe ese perfil.
guardar_analisis(), reportar(), cobertura_analisis()
resultado <- analizar( datos_administrativos, analizar_dependencias = FALSE, fecha = as.POSIXct("2026-01-15", tz = "UTC") ) resultadoresultado <- analizar( datos_administrativos, analizar_dependencias = FALSE, fecha = as.POSIXct("2026-01-15", tz = "UTC") ) resultado
Para cada columna temporal propone una frecuencia en días. La confianza es
el mínimo entre la contigüidad de las fechas sobre la grilla propuesta y la
cobertura del período: una coincidencia breve dentro de una serie muy
dispersa no puede producir confianza alta. Ambas componentes se devuelven
para que la propuesta sea auditable. La propuesta nunca queda confirmada
automáticamente. calendario usa días
ISO: 1 es lunes y 7 domingo; así una oficina puede declarar 1:5 sin que la
ausencia de fines de semana se interprete como hueco. Las fechas-hora se
llevan a fecha civil en UTC para que el resultado no dependa de la zona del
equipo que ejecuta el análisis.
analizar_tiempo( datos, perfil = NULL, columnas = NULL, calendario = 1:7, frecuencia_dias = NULL, max_huecos = 20L, max_columnas = 50L, proteger_datos_personales = TRUE, columnas_personales = character() )analizar_tiempo( datos, perfil = NULL, columnas = NULL, calendario = 1:7, frecuencia_dias = NULL, max_huecos = 20L, max_columnas = 50L, proteger_datos_personales = TRUE, columnas_personales = character() )
datos |
Tabla que se desea examinar. |
perfil |
Perfil opcional de los mismos datos. |
columnas |
Columnas temporales; |
calendario |
Días de semana esperados, enteros entre 1 y 7. |
frecuencia_dias |
Frecuencia entera conocida en días. Si es |
max_huecos |
Máximo de grupos de huecos devueltos por columna. |
max_columnas |
Máximo de columnas temporales analizadas. |
proteger_datos_personales |
Si se ocultan los rangos y los huecos de
las columnas temporales que son dato personal. |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
Objeto analisis_temporal con resumen, dias_semana, huecos y
propuestas. El recorte de huecos queda en resumen; el de columnas, en
atributos del objeto. resumen agrega tres campos sobre el alcance del
resumen, con el mismo vocabulario que usa perfilar() sobre la misma
columna: n_fechas_excluidas_parseo cuenta los valores presentes que
ningún formato confirmado pudo convertir y que por eso quedaron fuera;
n_fechas_excluidas_granularidad, cuántos de ésos son períodos de mes —
2024-02 y sus formas—, que no se convierten a propósito porque no
nombran un día; y estado_resumen vale "calculados_sobre_dias" cuando
hubo períodos de mes, "calculados_sobre_fechas_parseadas" cuando sólo
hubo valores ilegibles, y "calculados" cuando no quedó nada afuera. Los
tres se informan siempre, haya muestreo o no, porque el descarte tampoco
depende del muestreo. Un NA no entra en esta cuenta: es una ausencia
declarada y se informa como faltante, no como valor que no se pudo leer.
Una columna mixta —días y meses— sí se resume, sobre sus fechas
completas. Sólo cuando ningún formato confirmado nombra un día no hay serie
diaria que construir; entonces la columna no aparece en resumen, y se
declara en los atributos columnas_omitidas y columnas_sin_serie_diaria
en vez de desaparecer.
Si se recibe un perfil que declara proteccion para una columna temporal,
sus rangos y huecos se protegen tambien en esta llamada directa, con el
mismo texto que usa analizar(). Sin perfil, la columna temporal que es
un dato personal —una fecha de nacimiento— se clasifica aquí por léxico y
por forma, como en clasificar_variables(), y sus rangos y huecos se
protegen igual; lo que sólo quien conoce los datos sabe personal entra por
columnas_personales.
detectar_formatos_fecha(), analizar()
fechas <- as.Date("2026-01-01") + c(0:4, 20:24) analizar_tiempo(data.frame(fecha = fechas))fechas <- as.Date("2026-01-01") + c(0:4, 20:24) analizar_tiempo(data.frame(fecha = fechas))
Devuelve las 49 entradas del catálogo del marco y explicita qué parte está
disponible en lupa. Los ratios no se duplican como métricas: aparecen con
estado "via_agregacion" y con la llamada que los materializa.
catalogo_agesic()catalogo_agesic()
estado responde qué disponibilidad tiene cada entrada:
implementada: existe un motor ejecutable, completo o parcial;
via_agregacion: se obtiene agregando una métrica base;
pendiente: es automatizable, pero el motor necesario aún no existe;
fuera_de_alcance: se decidió no implementarla en el alcance tabular de
esta versión, por ejemplo métricas geográficas o de imágenes.
motivo separa la causa o el matiz: semantica_completa,
semantica_parcial, agregacion, requiere_referencial,
requiere_configuracion, motor_pendiente o decision_alcance. Así una
métrica ejecutable que necesita un padrón externo no se confunde con una
métrica cuyo motor falta. observacion explica la situación concreta de
cada una de las 49 entradas y nunca queda vacía.
Escala se clasifica como implementada con configuración experta mediante
escala(), no como referencial. DesactualizacionPorFecha,
DesactualizacionPorCambios y las oportunidades de entidad requieren un
contrato vigencia(). ErrorEstandar sigue la semántica literal de la tabla
16.5 y devuelve desviación estándar, aunque su nombre pueda sugerir el error
estándar de la media.
La implementación de ReglaIntegridadInterEntidad se declara parcial:
calcula cobertura PK/FK, pero todavía no materializa la regla booleana de
inclusión o expresión condicional con granularidad conjuntoEntidades que
define el marco. OportunidadAtributo*, en cambio, sigue las tablas 16.29 y
16.30 con resultado booleano. Las variantes continuas del curso CPAP se
conservan como GradoOportunidadAtributo* y no figuran como entradas del
catálogo. RatioDensidadPonderada usa ratio_umbral, porque su medida base
es real y ratio sólo es válido para medidas booleanas.
implementacion publica una receta, no una expresión que se evalúe sola: en
las entradas via_agregacion, m es la medición ya tomada de la métrica
base que nombra metrica_lupa y u el umbral que elige quien agrega. Son
los dos únicos nombres libres que aparecen, y ninguno se inventa aquí: m
sale de medir() y u es una decisión de política —cuánta densidad se
considera suficiente— que el paquete no toma por nadie.
Data frame con una fila por entrada y las columnas numero,
dimension, factor, metrica_agesic, clase_catalogo, estado,
motivo, metrica_lupa, implementacion y observacion. estado y
motivo son factores.
AGESIC (2020). Marco de trabajo para la Gestión de la Calidad de Datos en Gobierno Digital, versión 1.6, capítulo 16, Presidencia de la República, Uruguay.
metricas_nucleo(), metricas_referencial(), agregar()
catalogo <- catalogo_agesic() subset(catalogo, estado == "via_agregacion") table(catalogo$estado, catalogo$motivo)catalogo <- catalogo_agesic() subset(catalogo, estado == "via_agregacion") table(catalogo$estado, catalogo$motivo)
Separa el tipo de almacenamiento, el tipo implícito y la escala de medición.
Una propuesta basada sólo en valores nunca queda confirmada: 1, 2, 3 puede
ser conteo, código o nivel. Las clases ordered, factor, logical, las
clases temporales y el metadato measure sí constituyen declaraciones.
measure = "scale" se publica como continua, la lectura que SPSS da a ese
nivel. Una unidad declarada —el atributo units— establece una magnitud,
pero no si es discreta o continua: confirma continua cuando los valores
traen decimales, y con valores enteros la propuesta queda sin confirmar,
porque pueden ser una cuenta o una medida redondeada.
clasificar_variables( datos, perfil = NULL, metadatos = NULL, max_niveles = 100L, muestra = 1e+05, proteger_datos_personales = TRUE, columnas_personales = character() )clasificar_variables( datos, perfil = NULL, metadatos = NULL, max_niveles = 100L, muestra = 1e+05, proteger_datos_personales = TRUE, columnas_personales = character() )
datos |
Tabla que se desea clasificar. |
perfil |
Perfil opcional de los mismos datos. |
metadatos |
Declaraciones opcionales por columna. |
max_niveles |
Máximo de niveles guardados en cada lista. |
muestra |
Máximo de valores usados para enumerar niveles no declarados; los niveles ausentes de factores y metadatos se verifican sobre toda la columna. |
proteger_datos_personales |
Si se ocultan niveles concretos de columnas
cuya clasificación activa protección automática. Véase |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
Con perfil, una columna declarada en su columnas_codigo es un código y
no una magnitud: en lugar de discreta o continua con rol medida, sale
nominal, con rol categoria y confirmada, porque la declaración es del
usuario. metadatos la puede corregir igual.
metadatos permite confirmar o corregir la propuesta con una tabla editable.
Debe contener columna y puede incluir escala, rol, confianza,
confirmada, unidad y una columna de lista niveles. Las escalas válidas
son nominal, ordinal, discreta, continua, binaria, temporal y desconocida.
Los niveles declarados y observados se conservan como columnas de lista. Los
niveles ausentes son una observación, no prueba de error. Si la evidencia de
dato personal activa la protección, los niveles concretos se protegen, y los
valores de esas columnas escritos en los niveles de OTRA columna —una
observación que cita la cédula— se tapan con el mismo piso que aplica
perfilar(). Sólo
las escalas nominal, ordinal y binaria guardan niveles observados; en las
demás, n_niveles_observados es NA, porque la cifra no aplica.
Una columna numérica sin ningún valor finito —sólo Inf o -Inf— queda
desconocida: Inf no es un entero ni un estado.
Data frame S3 clasificacion_variables, editable y filtrable.
analizar(), distribucion_valores(), proponer_modelo()
d <- data.frame( prioridad = ordered(c("baja", "alta"), levels = c("baja", "media", "alta")), cantidad = c(1L, 2L) ) clasificar_variables(d)d <- data.frame( prioridad = ordered(c("baja", "alta"), levels = c("baja", "media", "alta")), cantidad = c(1L, 2L) ) clasificar_variables(d)
Devuelve una fila por dimensión y factor del marco_calidad() elegido.
Usa marco_agesic() por omisión, pero acepta cualquier taxonomía declarada.
Distingue
lo efectivamente medido de lo que no fue declarado, lo que no aplica a los
tipos presentes y lo que queda fuera del alcance actual. La tabla evita que
la ausencia de un hallazgo se interprete como evidencia de calidad.
cobertura_analisis(perfil, medicion = NULL, modelo = marco_agesic())cobertura_analisis(perfil, medicion = NULL, modelo = marco_agesic())
perfil |
Primer argumento. Objeto creado por |
medicion |
Segundo argumento, opcional. Objeto creado por
La correspondencia entre los dos objetos es suya, no del paquete. La cobertura aparea por dimensión y factor, y no verifica que la medición provenga de la tabla que se perfiló: si le pasa una medición de otra tabla, el factor va a figurar como medido por esa corrida. El paquete no puede comprobarlo porque un perfil no guarda una identidad de tabla comparable con la entidad de la medición. |
modelo |
Tercer argumento. Objeto creado por |
En el marco incluido, el profiling automático mide densidad y no
duplicación. Un marco propio puede marcar otros factores mediante la columna
perfil_mide. Los demás sólo pasan a "medida" cuando medicion contiene
una métrica del factor; descubrir un patrón o una dependencia no los
convierte por sí solo en un requisito confirmado.
Data frame con marco, dimension, factor, estado, motivo y
como_resolverlo. marco identifica explícitamente la taxonomía contra la
que se calculó la tabla. estado es un factor con niveles "medida",
"no_declarada", "no_aplica" y "fuera_de_alcance".
El motivo publica el alcance cuando el perfil resumió sólo parte de las
columnas. Si ninguna se pudo resumir, el estado es "no_aplica"; si se
resumieron todas, el motivo lo dice; y si se resumieron algunas, el estado
sigue siendo "medida" —el factor sí se midió, en las columnas que se
pudieron resumir— y el motivo empieza con "Parcial: " y nombra cuántas de
cuántas. Esas filas se encuentran con
grepl("^Parcial: ", cobertura$motivo). El estado no distingue el caso
parcial a propósito: es un factor de cuatro niveles documentados que
tablero_calidad() valida, y el grado —1 de 100 no es 99 de 100— no cabe
en un nivel. El alcance viaja en el motivo, y tablero_calidad() lo
conserva al cruzar la cobertura con su medición.
marco_calidad(), perfilar(), medir(), vigencia(), escala(),
reportar()
perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) cobertura_analisis(perfil)perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) cobertura_analisis(perfil)
Una colección es una base de datos entera: el séptimo nivel de granularidad
del marco. lupa no la descubre recorriendo el catálogo — la frontera la
declara quien conoce la base, igual que los pesos de indice_calidad() o
el marco de cobertura_analisis(). Explorar el catálogo convertiría un error
de permisos en un resultado, y en bases reales de más de mil tablas
recorrerlas todas no es viable.
coleccion(conexion, tablas, nombre = NULL)coleccion(conexion, tablas, nombre = NULL)
conexion |
Conexión abierta compatible con DBI. |
tablas |
Nombres de las tablas que componen la colección: un data frame
con columnas |
nombre |
Etiqueta de la colección. |
Esta función no consulta nada: sólo declara. Lo que se mide viene después,
con perfilar_coleccion().
El esquema es parte de la identidad de la tabla, y el catálogo también
cuando existe: catalogo.esquema.tabla es el nombre de tres partes que usan
SQL Server y otros motores. Se declara con la columna catalogo, y donde no
hay catálogo se omite —o va NA— y la identidad queda igual que siempre. Dos
tablas con el mismo nombre y esquema en catálogos distintos son dos tablas
distintas, y se cuentan como dos. Una tercera columna
tipo permite declarar qué es cada objeto —"tabla", "vista",
"temporal"—, porque el conteo bruto de un catálogo mezcla tablas base con
vistas, índices y secuencias, y no todas se perfilan igual.
Objeto de clase coleccion_lupa: una lista con nombre, la
conexion declarada, tablas -un data frame con una fila por tabla
declarada y sus columnas de esquema y nombre-, motor con la clase del
controlador DBI, y n_declaradas con la cantidad de tablas. No abre
consultas ni lee datos: sólo declara qué tablas forman la colección.
Hay tres formas, y no son equivalentes:
data.frame(esquema =, tabla =)La forma recomendada para cualquier nombre no trivial. Los nombres viajan literales: puntos, espacios y comillas incluidos. No hay parseo y por lo tanto no hay nada que se pueda parsear mal.
DBI::Id, suelto o en una listaLa forma canónica de DBI. Tampoco se parsea. Se admiten hasta tres componentes —catálogo, esquema y tabla—. Con cuatro o más se rechaza nombrando la causa: por encima del catálogo no hay un nivel que la colección sepa declarar, y devolver ese error como si fuera un problema de permisos mandaría a pedir un acceso que ya se tiene.
"esquema.tabla"
Atajo cómodo para el caso simple. El texto se parte en el punto, respetando el entrecomillado del motor: un nombre entrecomillado con un punto adentro queda entero, como una sola tabla. Un nombre de tres o más partes, un punto al principio o al final, o una comilla sin cerrar se rechazan acá, con el motivo real. No se aceptan para fallar más tarde como si fueran un problema de permisos.
El atajo de texto no puede resolver una ambigüedad genuina: "informe.2024"
puede ser la tabla 2024 del esquema informe o una tabla llamada
informe.2024. lupa elige la primera lectura y lo deja anotado, de modo
que si después no encuentra la tabla dice que el nombre se partió. Para el
caso ambiguo use el data frame.
perfilar_coleccion(), perfilar_dbi(), granularidades()
if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) coleccion(con, "personas", nombre = "padron") # La forma literal, sin parseo, para nombres con puntos: coleccion(con, data.frame(esquema = NA, tabla = "personas")) DBI::dbDisconnect(con) }if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) coleccion(con, "personas", nombre = "padron") # La forma literal, sin parseo, para nombres con puntos: coleccion(con, data.frame(esquema = NA, tabla = "personas")) DBI::dbDisconnect(con) }
Compara por intersección los campos registrados de dos perfiles y devuelve
una fila por cada par de columna y campo. Los campos que no tienen un eje
registrado no se comparan y quedan declarados en campos_no_comparables, y
lo mismo pasa con un campo registrado que uno de los dos lados no mide: se
declara ahi, con el motivo campo_solo_en_anterior o campo_solo_en_actual
en detalle_campos_no_comparables y una fila en cobertura_diagnosticos.
Los campos bajo protección tampoco se comparan: se declaran por columna,
campo y lado en campos_protegidos.
comparar_equivalencia(anterior, actual, tolerancia)comparar_equivalencia(anterior, actual, tolerancia)
anterior, actual
|
Un objeto |
tolerancia |
Número escalar no negativo y finito, declarado por quien llama. No tiene valor por omisión y se publica en cada fila. La tolerancia es mixta, no relativa. La diferencia se divide por el
mayor de |
El registro fijo asigna tolerancia sólo a media, mediana, desvio y
longitud_media. Los conteos, proporciones de conteos y extremos por
selección se comparan en el eje exacto; las fechas canónicas en fecha y
moda y centinela_valor en valor. Los ejes exacto, fecha y valor
son binarios por construcción.
El comparador devuelve datos, no decisiones: no alimenta hallazgos, severidades ni puntajes. La tolerancia es del llamador y jamás entra en una regla del paquete; sólo se aplica al eje flotante finito y queda publicada para que el llamador decida cómo usarla.
Si una columna es temporal en exactamente uno de los perfiles, los campos
de magnitud numérica se omiten por el cambio de esquema y su motivo queda en
detalle_campos_no_comparables como
tipo_cambiado:temporal_vs_no_temporal. Así no se comparan duraciones en
segundos contra magnitudes numéricas sin unidad común. Cuando ambos lados
son temporales, desvio sí se compara en flotante porque ambas puertas lo
expresan en segundos.
Si un lado es temporal y el otro no publica su tipo —el frame de
perfilar_dbi() no trae ningún campo de tipo—, no se afirma un cambio de
tipo: los mismos campos se omiten con el motivo
tipo_desconocido:temporal_vs_sin_tipo, porque tampoco se puede garantizar
una unidad común. Una fecha guardada como número en el motor tiene su
desvio en días de ese lado y en segundos del otro, y compararlos
publicaría como diferencia del dato una diferencia de unidad.
Si una columna guarda caracteres en exactamente uno de los perfiles, los
campos calculados sobre la representación —longitudes, variantes unicode y
números escritos como texto— se omiten y su motivo queda en
detalle_campos_no_comparables como tipo_cambiado:texto_vs_no_texto. Esos
campos cambian con sólo cambiar el almacenamiento, aunque el dato sea el
mismo, de modo que compararlos publicaría el síntoma y callaría la causa.
factor cuenta como almacenamiento de caracteres. Este guarda lee el tipo
declarado y no el inferido: si el texto contiene números o fechas, la
inferencia borra justo la diferencia que hay que ver. Cuando el tipo
declarado viene de un vocabulario ajeno al de memoria —un tipo SQL, por
ejemplo— no se afirma nada y la comparación sigue como siempre.
Por la misma razón, si una columna lleva zona horaria en exactamente uno de
los perfiles, los campos que sólo ese almacenamiento puede medir se omiten
con el motivo tipo_cambiado:con_zona_vs_sin_zona. Del otro lado no dan un
valor distinto: no dan ninguno, porque no hay zona que comparar contra UTC.
Los extremos de fecha sí se siguen comparando: al guardarse como texto
la columna pierde la zona y los instantes que denota son realmente otros,
que es lo más importante que hay para informar en ese caso.
Un frame de clase equivalencia_perfiles con columna, campo,
valor_anterior, valor_actual, diferencia_normalizada, veredicto,
motivo, tipo_eje y tolerancia. veredicto es un factor ordenado con
niveles identico < equivalente < materialmente_distinto. Los atributos
campos_no_comparables, detalle_campos_no_comparables,
columnas_no_comparables, cobertura_diagnosticos, campos_protegidos
y resumen declaran, respectivamente, los campos omitidos, los motivos
estructurales de esos campos, las columnas presentes en un solo lado o
con tipos incompatibles, los diagnósticos que no se pudieron evaluar, los
campos omitidos por protección y el conteo de cada veredicto.
Un campo sin valor medible en ambos lados, o con valor en un solo lado,
queda en la tabla con veredicto no_comparable y no suma como acuerdo.
campos_protegidos es un data frame con las columnas columna, campo y
lado; este último toma los valores anterior y actual.
comparar_perfiles(), perfilar(), perfilar_dbi()
anterior <- data.frame( columna = "monto", media = 10, minimo = 1, moda = "a", stringsAsFactors = FALSE ) actual <- data.frame( columna = "monto", media = 10.00000000001, minimo = 2, moda = "b", stringsAsFactors = FALSE ) comparar_equivalencia(anterior, actual, tolerancia = 1e-9)anterior <- data.frame( columna = "monto", media = 10, minimo = 1, moda = "a", stringsAsFactors = FALSE ) actual <- data.frame( columna = "monto", media = 10.00000000001, minimo = 2, moda = "b", stringsAsFactors = FALSE ) comparar_equivalencia(anterior, actual, tolerancia = 1e-9)
Calcula el cambio de EvaluacionPerfil entre dos corridas. Cada objeto debe
contener una sola id_medicion; no persiste los resultados. Para una serie
de N corridas use historico_calidad() y detectar_deriva_calidad().
comparar_evaluaciones(anterior, actual)comparar_evaluaciones(anterior, actual)
anterior, actual
|
Objetos creados por |
Data frame con resultados anterior y actual, delta y
comparacion. El delta es el que publica detectar_deriva_calidad() sobre
el histórico de las dos evaluaciones, con la misma regla: queda en NA
cuando las corridas miden tablas distintas, cambia el marco o los tipos de
resultado, cambia la parte medida de la frontera o un resultado no se
evaluó, y comparacion dice por qué con la palabra de la deriva. El orden
lo dan los argumentos, no la fecha: anterior es siempre el primero.
# Ver ejemplos de evaluar().# Ver ejemplos de evaluar().
Compara entregas sin exigir que tengan las mismas columnas. Devuelve cambios de esquema, tipos, faltantes, cardinalidad, rango, patrones y hallazgos como un objeto de datos filtrable.
comparar_perfiles(anterior, actual, umbral_cambio = 0.05, umbral_error = 0.2)comparar_perfiles(anterior, actual, umbral_cambio = 0.05, umbral_error = 0.2)
anterior, actual
|
Objetos producidos por |
umbral_cambio |
Diferencia mínima para considerar significativo un cambio de proporción, cardinalidad o rango relativo. Cinco puntos porcentuales evita elevar variaciones pequeñas a hallazgo. |
umbral_error |
Diferencia a partir de la cual un aumento de faltantes o
un cambio de patrón —uno que aparece, desaparece o cambia de proporción— se
clasifica como |
Los patrones se comparan sobre el resumen acotado que conserva cada perfil, no sobre los valores originales ni una distribución completa. Si las dos corridas usaron configuraciones de patrones diferentes, se informa un error de comparabilidad y esa parte de la comparación se omite.
Los nombres de columnas y las configuraciones que contienen texto del usuario se comparan por sus bytes, no por la marca de codificación ni por la intercalación del locale.
Eso alcanza mientras los bytes lleguen intactos, y hay un caso en que no
llegan y no depende de este paquete: el formato RDS 3 —el de saveRDS()
por omisión— anota la codificación nativa de quien escribe, y al leer
traduce desde ella el texto que no declara la suya. Un perfil guardado bajo
LC_CTYPE=C y releído bajo otro locale puede volver con el texto cambiado;
en Windows el cambio es silencioso, porque ahí la conversión no falla.
Cuando eso pasa, el perfil releído está alterado, y conviene saber qué
alcanza a ver esta función. Compara las propiedades medidas de cada
columna, los hallazgos, el alcance del resumen y las configuraciones
declaradas —no los valores uno por uno—: la columna aspecto del resultado
nombra, fila por fila, qué se comparó, y es la enumeración viva. Un cambio de texto que no
altera ninguna propiedad medida no aparece: por ejemplo, si la moda de una
columna pasa de "Basico" a "Casico" —mismo largo, misma forma de patrón,
mismos conteos—, la comparación devuelve cero filas. Por eso el remedio ante
una relectura sospechosa no es confiar en la comparación para detectarla.
El remedio es una línea, saveRDS(perfil, archivo, version = 2), porque ese
formato no anota codificación nativa y por lo tanto no traduce nada. La
persistencia propia del paquete —guardar_historico() y
guardar_analisis()— ya lo usa.
Las claves internas que agrupan hallazgos también se normalizan por bytes;
por eso dos perfiles idénticos no emiten avisos espurios bajo C.
Que la moda no se compare es una decisión, no un pendiente: la moda es un valor, no una propiedad medida, y cambia con cualquier corrimiento normal de los datos, así que compararla llenaría la deriva de filas que no son deterioro. Quien necesite detectar una relectura corrompida tiene el remedio de una línea que está dos párrafos más abajo, que la evita en lugar de detectarla.
Entre las propiedades comparadas está la unidad en la que la columna
publica sus cifras, con la misma severidad que un cambio de tipo: dos entregas
con los mismos valores y la unidad cambiada de m a km son un cambio real
—la cifra dejó de significar lo mismo— y antes devolvían cero filas. Un perfil
guardado antes de que ese campo existiera no hace mentir la comparación: esa
parte se declara no comparable, como el resto de los campos ausentes.
El rango se compara por los valores de sus extremos, no por su texto
impreso: un cambio chico sale como fila ok y uno que alcanza
umbral_cambio —medido contra el ancho del rango anterior— como
sospechoso, sin que options(scipen) ni digits muevan el veredicto.
Cuando el perfil conserva los extremos exactos de un entero que no entra en
un doble —un integer64 por encima de 2^53, o un entero escrito como
texto—, esos extremos se comparan con aritmética exacta. Los números se
publican con la escritura más corta que vuelve al valor, así que dos
extremos distintos nunca se imprimen iguales; la marca decimal sigue a
OutDec, y con coma decimal los extremos se separan con punto y coma
([1,5; 3]).
Una columna protegida no publica su rango, y la comparación tampoco lo
compara: lo declara en el atributo campos_protegidos, con la misma regla
que comparar_equivalencia(). Si la protección de una columna cambia entre
las corridas con la misma política —por ejemplo, porque la proporción de
documentos válidos cruzó el umbral de verificación—, el cambio se informa
con una fila propia, aspecto = "proteccion" y severidad sospechoso, en
lugar de un cambio de rango que no ocurrió en los datos. Si lo que cambió es
la política entera, ya lo dice la fila configuracion_proteccion y no se
repite por columna.
Además de los patrones que aparecen y desaparecen, se informa el cambio de
proporción de un patrón que sigue en el resumen cuando alcanza
umbral_cambio: sospechoso, o error desde umbral_error, en las dos
direcciones. Que los cambios menores no generen fila es una decisión, a
diferencia de los faltantes: serían una fila por patrón en cada entrega. Si
el resumen de un lado estaba lleno —la columna tenía más formas que
max_patrones—, un patrón que falta en él pudo quedar por debajo del corte,
y no se afirma que apareció o desapareció: la fila dice no_comparable, con
severidad ok, salvo que el cambio que sí se puede garantizar —contra la
menor proporción que guardaba ese resumen— alcance el umbral; entonces dice
modificado y delta es esa cota.
La cardinalidad no tiene una dirección buena: que caiga la tasa de
valores distintos de un identificador es un deterioro, y que suba la de una
columna de categorías también. Por decisión, un cambio significativo de
cardinalidad es sospechoso en los dos sentidos. Los faltantes sí la
tienen: que bajen es una mejora y queda ok.
Las columnas que aparecen o desaparecen generan cambios estructurales de
severidad error, pero no impiden comparar las columnas compartidas. Un
hallazgo de una columna retirada no se presenta como resuelto.
Un hallazgo que ya no aparece se informa como resuelto sólo si el
diagnóstico volvió a evaluarse. Si el perfil nuevo lo declinó —y lo dice en
su cobertura_diagnosticos—, el cambio se informa como no_evaluado con
severidad sospechoso, porque no se sabe si el hallazgo sigue: dejar de
mirar no es lo mismo que arreglar. Lo mismo si el perfil nuevo apagó el
diagnóstico con su argumento o retiró la declaración que lo habilita, cosa
que se lee en meta: casi_duplicados_vocabulario = FALSE,
ausencia_estructural = FALSE, variantes_equifrecuentes_vocabulario = FALSE, una clave retirada o declarada sobre otras columnas, una columna
retirada de columnas_sin_ceros, columnas_no_negativas,
columnas_personales o aplicabilidad, y duplicados_aproximados apagado o
corrido sobre otras columnas —lo que corrió queda en
perfil$duplicados_aproximados; si corrió sobre las mismas columnas con
otros parámetros, la fila configuracion_duplicados_aproximados lo
declara—; la descripción nombra el argumento. Al revés, un hallazgo que
aparece porque el perfil nuevo encendió o declaró lo que lo habilita sigue
saliendo aparecido, y su descripción dice que el anterior no lo evaluaba.
Un hallazgo de varias columnas —un par de columnas duplicado, una relación
entre columnas— se compara como cualquier otro: sólo no se presenta como
resuelto si alguna de sus columnas desapareció. Cada argumento de
perfilar() tiene su lugar en la comparación:
habilita un diagnóstico, es una vara —su cambio se declara en la fila
configuracion_umbrales, que desde esta versión incluye los umbrales de las
relaciones de orden y aritméticas entre columnas, los del vocabulario y
datos_personales_permitidos, validadores_personales,
umbral_documento_verificado y muestra_validadores—, tiene su propia fila
de configuración, acota el trabajo y lo declara en cobertura_diagnosticos,
o no cambia los hallazgos. Una vara que uno de los dos perfiles no registra
—un perfil de una versión anterior— no se compara.
Los hallazgos que cuentan filas de la tabla y no pertenecen a una columna
concreta tampoco se declaran resueltos si aparecieron o desaparecieron
columnas entre las corridas: cambio el universo que define esas filas. En
ese caso se publica no_comparable, con severidad error, y se nombran las
columnas cambiadas. Con las mismas columnas, el veredicto resuelto se
conserva.
Data frame deriva_perfil. severidad usa el factor ordenado
ok < sospechoso < error; los cambios menores permanecen como filas ok
para que la serie sea exportable sin ocultar diferencias.
El atributo campos_protegidos declara lo que no se comparó porque la
protección de datos personales lo tapa: un data frame con columna,
campo y lado —anterior o actual—, la misma forma que usa
comparar_equivalencia(). Hoy el único campo que puede figurar es
rango. Está siempre, vacío si no hay nada que declarar, y rbind() une
los de todas las derivas.
perfilar(), detectar_deriva_calidad(), reportar(),
comparar_equivalencia()
anterior <- perfilar(data.frame(codigo = c("AA1", "AA2")), fecha = as.POSIXct("2026-01-01", tz = "UTC")) actual <- perfilar(data.frame(codigo = c("AA1", "B-2"), nueva = 1:2), fecha = as.POSIXct("2026-02-01", tz = "UTC")) comparar_perfiles(anterior, actual)anterior <- perfilar(data.frame(codigo = c("AA1", "AA2")), fecha = as.POSIXct("2026-01-01", tz = "UTC")) actual <- perfilar(data.frame(codigo = c("AA1", "B-2"), nueva = 1:2), fecha = as.POSIXct("2026-02-01", tz = "UTC")) comparar_perfiles(anterior, actual)
vigencia() reúne el contrato temporal que las métricas de actualidad y
oportunidad no pueden inferir de los datos: columna de actualización,
fecha de acceso, último cambio conocido, fecha límite, intervalo y frecuencia
esperada. Cada métrica valida los campos que necesita y se abstiene si faltan:
declara en cobertura_metricas cuál falta y cómo declararlo, en lugar de
abortar la medición de las demás métricas.
vigencia( columna_actualizacion, fecha_acceso = Sys.time(), fecha_ultimo_cambio = NULL, fecha_limite = NULL, inicio_intervalo = NULL, fin_intervalo = NULL, frecuencia_cambio = NULL ) escala(error, tipo = c("absoluto", "relativo"))vigencia( columna_actualizacion, fecha_acceso = Sys.time(), fecha_ultimo_cambio = NULL, fecha_limite = NULL, inicio_intervalo = NULL, fin_intervalo = NULL, frecuencia_cambio = NULL ) escala(error, tipo = c("absoluto", "relativo"))
columna_actualizacion |
Nombre de la columna Date o POSIXt que registra la última actualización de cada fila. |
fecha_acceso |
Momento de acceso usado para estimar actualidad. De la
misma clase que la columna de actualización: con una columna |
fecha_ultimo_cambio |
Fecha conocida del último cambio en el mundo real; puede ser escalar o tener una entrada por fila. |
fecha_limite |
Fecha límite escalar o por fila para oportunidad. |
inicio_intervalo, fin_intervalo
|
Extremos del intervalo de vigencia. |
frecuencia_cambio |
Frecuencia esperada como |
error |
Error no negativo escalar, vectorial o función del valor. |
tipo |
Interpretación |
Las dos puntas de cada comparación tienen que ser de la misma clase. Un
Date se ancla a la medianoche UTC y un POSIXct vale por su instante, así
que mezclarlos puede hacer contar como tarde una actualización del día del
límite, y si el POSIXct se leyó sin huso el resultado depende de la sesión.
Cuando una métrica compara clases distintas lo avisa. Como fecha_acceso vale
Sys.time() por omisión, con una columna Date conviene declararlo también
como Date: fecha_acceso = Sys.Date().
escala() declara el error de un instrumento o de otra escala experta. Con
error absoluto, Escala calcula 1 - error / abs(valor) y acota el resultado
a [0, 1]; con error relativo calcula 1 - error. No se aprende el error de
la distribución observada.
vigencia() devuelve un objeto vigencia_datos; escala() devuelve
un objeto escala_medicion. Ambos son contratos de configuración y no
examinan datos.
metricas_nucleo(), especializar(), cobertura_analisis()
contrato <- vigencia( "actualizado", fecha_limite = as.Date("2026-02-01"), frecuencia_cambio = 30, fecha_acceso = as.Date("2026-02-15") ) instrumento <- escala(error = 0.5, tipo = "absoluto")contrato <- vigencia( "actualizado", fecha_limite = as.Date("2026-02-01"), frecuencia_cambio = 30, fecha_acceso = as.Date("2026-02-15") ) instrumento <- escala(error = 0.5, tipo = "absoluto")
Conjunto enteramente sintético para demostrar el motor de profiling. Incluye cédulas con formatos heterogéneos, fechas mezcladas, faltantes disfrazados, una columna constante, valores extremos, una fila duplicada y registros con el mismo identificador pero contenidos distintos. No representa personas ni registros de ningún organismo.
datos_administrativosdatos_administrativos
Un data frame con 13 filas y 10 variables:
Identificador interno, con una repetición contradictoria.
Documento sintético con formatos correctos e incorrectos.
Fechas en varios formatos y un faltante disfrazado.
Categoría sintética con un faltante disfrazado.
Importes con sentinelas numéricos y un valor extremo.
Categoría administrativa.
Columna constante.
Direcciones sintéticas y un patrón anómalo.
Copia redundante del identificador interno.
Identificador sintético de alta unicidad.
Generación sintética incluida con el paquete.
perfilar(), planificar_limpieza()
data(datos_administrativos) perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) perfil$columnas[, c("columna", "tipo_inferido", "prop_faltantes_totales")]data(datos_administrativos) perfil <- perfilar(datos_administrativos, analizar_dependencias = FALSE) perfil$columnas[, c("columna", "tipo_inferido", "prop_faltantes_totales")]
Conjunto enteramente sintético, sin vocabulario ni convenciones de un país,
para demostrar el recorrido general de lupa. Siembra los mismos tipos de
problemas que el ejemplo administrativo: formatos heterogéneos, fechas
mezcladas, ausentes disfrazados, espacios y mayúsculas inconsistentes,
valores extremos, una columna constante, una columna duplicada, una fila
duplicada y un identificador repetido con datos contradictorios. No representa
personas, operaciones ni registros reales.
datos_operativosdatos_operativos
Un data frame con 13 filas y 10 variables:
Identificador interno, con una repetición contradictoria.
Código sintético con formatos heterogéneos y un ausente disfrazado.
Fechas en varios formatos y un ausente disfrazado.
Categoría con variantes de capitalización, espacios y ausentes disfrazados.
Importes con sentinelas, cero, un negativo y un valor extremo.
Categoría operativa ficticia.
Columna constante.
Direcciones sintéticas y un patrón anómalo.
Copia redundante del identificador interno.
Identificador sintético de alta unicidad.
Generación sintética incluida en data-raw/datos_operativos.R.
datos_administrativos, analizar(), perfilar()
data(datos_operativos) analisis <- analizar(datos_operativos, analizar_dependencias = FALSE) analisis$perfil$hallazgos[, c("columna", "tipo_hallazgo", "severidad")]data(datos_operativos) analisis <- analizar(datos_operativos, analizar_dependencias = FALSE) analisis$perfil$hallazgos[, c("columna", "tipo_hallazgo", "severidad")]
Generaliza un vector de texto mediante la convención del Pattern Finder
de DataCleaner: 9 representa un dígito, a una letra minúscula y A una
letra mayúscula. Los símbolos y espacios se conservan literalmente. Las tres
clases valen en cualquier escritura: un dígito arábigo-índico es 9 y una
é es a, de modo que el patrón no publica letras del valor; una letra sin
mayúscula ni minúscula —árabe, CJK— cuenta como a.
descubrir_patrones( x, distinguir_mayusculas = TRUE, expandir = FALSE, max_patrones = 20, na.rm = TRUE, muestra = 1e+05, umbral_raro = 0.05, proteger_datos_personales = TRUE )descubrir_patrones( x, distinguir_mayusculas = TRUE, expandir = FALSE, max_patrones = 20, na.rm = TRUE, muestra = 1e+05, umbral_raro = 0.05, proteger_datos_personales = TRUE )
x |
Vector que se convertirá a texto. |
distinguir_mayusculas |
Si es |
expandir |
Si es |
max_patrones |
Número máximo de patrones que se muestran: un entero
positivo, o |
na.rm |
Si es |
muestra |
Máximo de valores que se analizan. |
umbral_raro |
Frecuencia máxima de un patrón raro, usada para conservar un resumen acotado de patrones raros para los hallazgos. La frontera es inclusiva: un patrón no dominante con exactamente esa proporción es raro. |
proteger_datos_personales |
Si los ejemplos se enmascaran cuando la
forma de los valores alcanza para clasificarlos como dato personal. |
El cálculo aplica reemplazos vectorizados sobre el vector completo. Si el
vector supera muestra, usa una muestra sistemática reproducible y registra
esa decisión en los atributos del resultado.
Un data frame de clase patrones con patrón, frecuencia, proporción
y ejemplos. Los ejemplos no se publican cuando la forma de los valores
alcanza por sí sola para clasificarlos como dato personal —un correo, por
ejemplo, aunque los correos no sean la mayoría del vector—: salen como
[valor protegido], los de todos los patrones. Acá llega un vector suelto, sin
nombre de columna, así que la vía por nombre no está disponible; un número
de ocho dígitos sí se publica, porque su forma sola no alcanza para
afirmar que es un documento. Los atributos total, analizados, filas_analizadas y
muestreado describen el posible muestreo; filas_analizadas es un alias
explícito de analizados para mantener el alcance visible junto a otros
diagnósticos. n_evaluados es el denominador de proporcion: los valores
analizados, sin los ausentes cuando na.rm = TRUE. resumen_patrones
conserva sólo el patrón dominante
y hasta seis patrones raros para presentacion; nunca guarda la distribucion
completa. patrones_raros_trazabilidad conserva solo los nombres de los
patrones raros, hasta 5.000, para que la trazabilidad pueda enumerar filas
sin retener frecuencias ni ejemplos. Las proporciones siempre estan en
[0, 1]. n_patrones_distintos registra el total antes de truncar la tabla
para informar omisiones sin retenerla. n_patrones_raros y
n_patrones_raros_trazabilidad registran cuantos patrones raros habia antes
de sus respectivos limites. n_filas_patrones_no_dominantes_excluidos
registra cuantas filas pertenecen a patrones no dominantes cuya proporcion
no es rara y que por eso quedan fuera del hallazgo.
perfilar(), inferir_tipo(), detectar_formatos_fecha()
descubrir_patrones( c("2020-01-31", "2021-12-01", "31/01/2020"), expandir = TRUE )descubrir_patrones( c("2020-01-31", "2021-12-01", "31/01/2020"), expandir = TRUE )
Calcula Pearson —o Spearman, si se pide— entre numéricas, V de Cramér entre
categóricas y eta cuadrado entre una categórica y una numérica. Las medidas se
informan en [0, 1]: la correlación usa su valor absoluto. La tabla declara
el método, su supuesto, el soporte y el posible muestreo; no presenta
significancia estadística.
detectar_asociaciones( datos, dependencias = NULL, umbral = 0.3, muestra = 10000, max_columnas = 50L, max_niveles = 50L, max_pares = 500L, metodo_numerico = c("pearson", "spearman") )detectar_asociaciones( datos, dependencias = NULL, umbral = 0.3, muestra = 10000, max_columnas = 50L, max_niveles = 50L, max_pares = 500L, metodo_numerico = c("pearson", "spearman") )
datos |
Tabla que se desea examinar. |
dependencias |
Resultado opcional de |
umbral |
Valor mínimo en |
muestra |
Máximo común de filas; |
max_columnas |
Máximo de columnas analizables. |
max_niveles |
Máximo de niveles para tratar una columna como categórica. |
max_pares |
Máximo de asociaciones devueltas después de ordenar. |
metodo_numerico |
Medida entre columnas numéricas: |
metodo_numerico = "spearman" mide asociación monótona sobre los rangos
y no supone linealidad, así que reconoce una relación creciente aunque sea
curva. Pearson sigue siendo el valor por omisión porque es lo que la mayoría
espera de una correlación, y el método elegido viaja en la columna metodo
de la salida para que ninguna lectura dependa de recordar cuál se pidió.
Se descartan constantes, fechas, listas, columnas compuestas —matrices o
arreglos de más de una dimensión—, categóricas de cardinalidad alta y
columnas posteriores a max_columnas antes de construir pares. Las
dependencias funcionales exactas recibidas en dependencias no se repiten
como asociaciones.
Un par en el que una columna toma un solo valor en las filas completas no se
puede medir —ni Cramér ni eta cuadrado están definidos con una sola
categoría—, aunque la columna no sea constante en la tabla entera: va a
pares_omitidos_medicion y no se publica como asociación cero.
Data frame S3 asociaciones_columnas. Sus atributos declaran filas,
columnas y pares examinados, omisiones por dependencia y truncamiento.
pares_omitidos_medicion conserva los pares que no se pudieron medir,
junto con el motivo y la cantidad de filas completas disponible. Su
atributo cobertura_diagnosticos tiene una fila por par omitido. Cuando
analizar() integra el resultado, esas filas se agregan a la cobertura
del perfil.
detectar_dependencias(), analizar()
d <- data.frame(x = 1:20, y = 2 * (1:20), grupo = rep(c("A", "B"), 10)) detectar_asociaciones(d, umbral = 0)d <- data.frame(x = 1:20, y = 2 * (1:20), grupo = rep(c("A", "B"), 10)) detectar_asociaciones(d, umbral = 0)
Busca primero claves simples y luego combinaciones mínimas de dos o tres
columnas. No prueba una combinación si ya contiene una clave candidata más
pequeña. Una clave exige ausencia de NA y unicidad en todas las filas.
Además informa columnas simples casi-clave cuando tienen al menos 100 filas,
al menos el 90 % de sus valores son distintos y un único valor concentra al
menos la mitad de los duplicados excedentes. La concentración evita confundir
texto libre de alta cardinalidad, con muchas colisiones dispersas, con una
clave dañada. Las variables con rol propuesto fecha, incluidas fecha-hora,
no se consideran casi-claves.
Los faltantes disfrazados —"SIN DATO", -999— no cuentan como colisiones:
se reconocen con la misma máscara que perfilar(), con su política por
omisión o con la del perfil que se pase, y la casi-clave se mide sin ellos
—sus distintos, exactos y normalizados, y sus colisiones—. Una clave sana con
treinta documentos en "SIN DATO" no es una clave rota: le faltan treinta
documentos. n_filas sigue siendo el de la tabla.
La unicidad normalizada compara cada valor de texto por su clave de bytes,
la misma con que perfilar() cuenta n_distintos_normalizados: un valor
con marca bytes entra con cada byte alto escrito \xHH y no se pliega con
el mismo texto marcado UTF-8; latin1 y UTF-8 del mismo texto entran igual.
Los vectores double sólo son candidatos si ninguno de sus valores finitos
tiene parte fraccionaria. Esto conserva identificadores enteros importados
desde archivos de texto y excluye importes, coordenadas y otras medidas. Los
vectores integer64 se tratan como enteros semánticos.
detectar_claves(datos, max_combinacion = 3, normalizar = NULL, perfil = NULL)detectar_claves(datos, max_combinacion = 3, normalizar = NULL, perfil = NULL)
datos |
Objeto que hereda de |
max_combinacion |
Máximo de columnas por combinación, entre 1 y 3. |
normalizar |
Perfil de comparación. |
perfil |
Perfil producido por |
Dos claves simples se marcan como redundantes cuando sus contenidos son
idénticos —incluidas clase, atributos, ausencias y representación exacta—,
aunque tengan nombres distintos. Las columnas compuestas —matrices o
arreglos de más de una dimensión— y las de lista no se interpretan como
claves. Los pares también quedan en el atributo
claves_redundantes.
Una columna cuyo nombre se repite en la tabla no se analiza: una clave se
publica por el nombre de sus columnas, y ese nombre no diría cuál de las dos
identifica. Se avisa, y los nombres quedan en el atributo
columnas_nombre_repetido.
Data frame de claves candidatas y casi-claves con las columnas
combinadas, cantidad de columnas, marcas de redundancia, casi_clave,
unicidad_exacta y unicidad_normalizada. Las columnas de colisiones
publican sus valores, frecuencias y la concentración observada. Las claves
exactas conservan casi_clave = FALSE; una fila con casi_clave = TRUE
es un diagnóstico que requiere corregir o confirmar, no una clave válida.
detectar_dependencias(), detectar_relaciones()
detectar_claves(data.frame(id = 1:4, grupo = c("a", "a", "b", "b")))detectar_claves(data.frame(id = 1:4, grupo = c("a", "a", "b", "b")))
Busca pares ordenados determinante -> dependiente. El cumplimiento es la
proporción de filas que coincide con el valor modal del dependiente para
cada valor del determinante; por eso una dependencia aproximada señala
directamente las filas minoritarias que pueden ser errores de carga.
detectar_dependencias( datos, umbral = 0.995, muestra = 1e+05, max_columnas = 100L, umbral_casi_constante = 0.95, umbral_casi_clave = 0.8, incluir_claves = FALSE, min_observaciones = 10L, max_ejemplos = 5L, max_comparaciones = 200000L, max_trabajo = 1e+08, proteger_datos_personales = TRUE, columnas_personales = character() )detectar_dependencias( datos, umbral = 0.995, muestra = 1e+05, max_columnas = 100L, umbral_casi_constante = 0.95, umbral_casi_clave = 0.8, incluir_claves = FALSE, min_observaciones = 10L, max_ejemplos = 5L, max_comparaciones = 200000L, max_trabajo = 1e+08, proteger_datos_personales = TRUE, columnas_personales = character() )
datos |
Tabla que se desea examinar. |
umbral |
Cumplimiento mínimo en |
muestra |
Máximo de filas; |
max_columnas |
Máximo de columnas analizadas. |
umbral_casi_constante |
Proporción modal a partir de la cual un determinante se descarta por casi constante. |
umbral_casi_clave |
Tasa de valores distintos a partir de la cual un
determinante se descarta por casi clave, salvo que |
incluir_claves |
Si se incluyen determinantes únicos, que satisfacen dependencias de forma trivial. |
min_observaciones |
Mínimo de pares presentes para informar una dependencia. |
max_ejemplos |
Máximo de contradicciones concretas en |
max_comparaciones |
Máximo de pares determinante-dependiente que se
comparan. |
max_trabajo |
Máximo de unidades fila-par estimadas. |
proteger_datos_personales |
Si los valores de las columnas clasificadas
como dato personal se ocultan en |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
Para evitar resultados vacíos o triviales, se omiten por defecto las claves
únicas, los determinantes cuya tasa de valores distintos alcanza
umbral_casi_clave, los determinantes cuya moda alcanza
umbral_casi_constante y los dependientes constantes. El
valor predeterminado de umbral = 0.995 exige que como máximo 5 de cada
1.000 filas contradigan la relación. Los ausentes de cualquiera de las dos
columnas no integran el cálculo.
El descarte ocurre antes de construir agrupaciones. El valor predeterminado
umbral_casi_clave = 0.8 excluye determinantes con menos de 1,25 filas por
valor distinto en promedio: aun si cumplen, suelen describir una casi-clave
y no una regla reutilizable.
Las columnas compuestas —matrices o arreglos de más de una dimensión— no se
toman como determinantes ni dependientes: no son valores escalares por fila.
El costo crece aproximadamente como columnas^2 * filas. max_columnas
conserva las primeras columnas analizables, max_comparaciones limita el
número de pares columna a columna y muestra aplica una única muestra
sistemática a toda la tabla, de modo que las relaciones entre filas no se
rompen. max_trabajo acota el producto entre pares posibles y filas
analizadas. Es un presupuesto estimado, no una medición de segundos: permite
que el tope efectivo de pares baje cuando la tabla tiene muchas filas. Los
atributos del resultado declaran todos los recortes.
Una muestra conserva cada fila entera, pero no la cantidad de filas por
valor: sobre datos agrupados en filas consecutivas, la muestra sistemática
toma una fila de cada grupo y el determinante parece una clave. Por eso los
descartes por casi-clave y casi-constante se verifican sobre la columna
completa; si sólo la muestra los produjo, el motivo es
casi_clave_solo_en_muestra o casi_constante_solo_en_muestra, la columna
sigue fuera —evaluarla sobre esa muestra daría dependencias exactas sin
evidencia— y perfilar() lo declara en cobertura_diagnosticos.
Data frame de clase dependencias_funcionales, ordenado por
cumplimiento y soporte. evidencia cita contradicciones concretas, y no
publica los valores de una columna clasificada como dato personal: la
evidencia de un par que nombra una columna personal sale entera como
[evidencia protegida] —lo mismo que en el perfil— y, en las demás, lo que
repite un valor de una columna personal de la tabla —una observación que
cita la cédula— se tapa con el mismo piso que aplica perfilar(). Los atributos muestreado, filas_analizadas,
columnas_analizadas, columnas_omitidas, columnas_descartadas y
truncado documentan el alcance efectivo. n_pares_posibles,
n_pares_comparados, n_pares_sin_comparar y max_comparaciones
documentan el presupuesto de comparaciones. trabajo_estimado,
trabajo_comparado, trabajo_sin_comparar, unidad_trabajo y
max_trabajo documentan el presupuesto por filas. columnas_descartadas
es un data frame que explica por qué una columna no se usó como
determinante. Una columna cuyo nombre se repite en la tabla no entra en
ninguna dependencia, de ningún lado, y se descarta con el motivo
nombre_repetido: la fila diría k -> k sin decir cuál columna es cuál.
detectar_claves(), proponer_modelo(), perfilar()
datos <- data.frame( codigo = rep(1:3, each = 4), descripcion = rep(c("A", "B", "C"), each = 4), valor = seq_len(12) ) detectar_dependencias(datos, min_observaciones = 4)datos <- data.frame( codigo = rep(1:3, each = 4), descripcion = rep(c("A", "B", "C"), each = 4), valor = seq_len(12) ) detectar_dependencias(datos, min_observaciones = 4)
Compara corridas consecutivas, ordenadas por fecha dentro de cada perfil o
regla, y marca cambios significativos en la escala [0, 1]. Los empates de
fecha se resuelven por los bytes de id_medicion, de modo que la misma
secuencia persiste igual bajo cualquier locale.
detectar_deriva_calidad(historico, nivel = c("perfil", "regla"), umbral = 0.05)detectar_deriva_calidad(historico, nivel = c("perfil", "regla"), umbral = 0.05)
historico |
Objeto creado por |
nivel |
|
umbral |
Cambio absoluto mínimo considerado significativo. El valor
predeterminado de |
Data frame deriva_calidad con una fila por par de corridas
consecutivas. Una mejora significativa conserva severidad ok; un
deterioro de al menos un umbral es sospechoso y uno de al menos dos
umbrales es error. identidad_tabla separa series de tablas distintas y
aspecto marca el resultado o un cambio de configuracion; cambio usa
no_comparable cuando una configuracion del modelo impide comparar las
corridas —cambió el marco o los tipos de resultado, y entonces la fila de
resultado no se publica—. Cualquier otro cambio de modelo se declara en su
fila configuracion_modelo sin esa etiqueta: la comparación se mantiene, y
la fila de resultado del mismo par conserva su veredicto. Limitación
conocida: una corrida guardada por una versión anterior del paquete cuya
configuración lleva un número que con 15 cifras no vuelve a su valor
—maximo = 1/3, 0.1 + 0.2, un umbral calculado con quantile()— o un
instante con fracción de segundo declara una vez esa fila, con
severidad error, contra la primera corrida nueva: la versión anterior
escribía el número con 15 cifras (0.333333333333333) y el instante cortado
al segundo, y la actual con las cifras que los devuelven a su valor
(0.3333333333333333). La fila de resultado de ese par conserva su
veredicto, y desde ahí la serie aparea.
Un par que no se puede comparar no recibe veredicto. Si alguna de las
dos corridas no evaluó su resultado, delta, cambio_absoluto,
significativo, direccion y severidad quedan en NA —no en estable
ni en ok— y descripcion nombra de qué lado falta el resultado. Filtrar
por severidad != "ok" deja fuera esas filas a propósito: no son filas
sanas, son filas sin medición, y se encuentran con is.na(significativo).
La comparación es entre corridas consecutivas: una corrida sin evaluar
en medio de la serie deja sin veredicto sus dos pares y la deriva no
compara por encima de ella. Para comparar las corridas de los dos lados,
quitar la del medio del histórico —h[h$id_medicion != "B", ]—.
Lo mismo cuando a una de las dos corridas le falta su configuración —una
tabla armada a mano, o un CSV exportado sin las columnas identidad_tabla
y configuracion_*—: sin ella no se sabe si las dos miden la misma tabla
con el mismo marco, así que cambio es no_comparable, el veredicto queda
en NA y el atributo cobertura_diagnosticos lo declara.
Un cambio de frontera tampoco se compara. Si las dos corridas no dejan
afuera las mismas partes declaradas —el nivel parte_no_medida del
histórico: una tabla de la colección que entró o salió del número—, sus
números no cubren lo mismo: la fila de resultado no se publica y una fila
aspecto = "cobertura_frontera", cambio = "no_comparable", lo declara,
con las partes sin medir de cada lado en evidencia. Dos corridas a las que
les falta la misma parte sí se comparan.
# El ejemplo de historico_calidad() muestra cómo construir las corridas.# El ejemplo de historico_calidad() muestra cómo construir las corridas.
Recorre las señales declaradas con senal_redundante() e informa las filas
donde las columnas que deberían codificar el mismo hecho no concuerdan dentro
de la ventana declarada.
detectar_discordancias( datos, senales, max_ejemplos = 5L, proteger_datos_personales = TRUE, columnas_personales = character() )detectar_discordancias( datos, senales, max_ejemplos = 5L, proteger_datos_personales = TRUE, columnas_personales = character() )
datos |
Tabla que se desea examinar. |
senales |
Una señal creada por |
max_ejemplos |
Máximo de filas concretas que se citan como evidencia:
un entero no negativo; |
proteger_datos_personales |
Si los valores de las columnas clasificadas
como dato personal se enmascaran en |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
El valor de este diagnóstico es que ninguna columna por separado lo muestra: los tres años de una fila pueden ser todos plausibles y aun así contradecirse entre sí.
Una fila donde alguna de las columnas comparadas está ausente no se cuenta
como discordante ni como concordante: se excluye del universo evaluado, y
n_evaluadas lo declara. Ausencia no es desacuerdo.
Una columna de la señal que no tiene un valor comparable por fila —una
compuesta (matriz, tabla anidada, valores que no se escriben como texto), una
lista de objetos o de geometrías, o una magnitud con unidad (units), cuyo
número sin la unidad no es su valor— no entra: la fila de la señal sale con
n_evaluadas = 0, n_discordantes = NA y la evidencia dice qué columna y
por qué. Con una transformacion que la lleve a un valor por fila —por
ejemplo units::drop_units() después de llevarla a una unidad—, se compara.
Una fila con el mismo valor en todas las columnas concuerda, también si ese
valor es infinito: Inf contra Inf es coincidencia exacta.
Una fila citada no muestra iguales dos valores que el veredicto separó. Si la
escritura corriente los junta —dos dobles que coinciden en 15 cifras, dos
instantes del mismo segundo, dos textos que se ven iguales con distintos
puntos de código—, esa fila los cita con la escritura que los distingue: el
doble con las cifras que lo devuelven a su valor, el instante con su fracción
y el texto con cada carácter fuera del ASCII imprimible escrito <U+XXXX>,
con una nota que lo dice.
Data frame con una fila por señal: senal, columnas, n_filas,
n_evaluadas, n_discordantes, proporcion, ventana y evidencia.
evidencia cita filas concretas, y no publica los valores de una columna
clasificada como dato personal —salvo que se pida lo contrario con
proteger_datos_personales = FALSE—: esos salen como [valor protegido]. El
número de fila y el nombre de la columna se conservan, que es lo que hace
falta para ir a corregirla. Las columnas que no son personales se citan
enteras, salvo lo que en ellas repite un valor de una columna personal de
la tabla —una observación que cita la cédula—: eso se tapa con el mismo
piso que aplica perfilar(), aunque la columna personal no esté en la
señal.
senal_redundante(), detectar_dependencias(),
detectar_relaciones()
d <- data.frame( anio_fiscal = c(2023L, 2023L, 2022L), anio_archivo = c(2023L, 2023L, 2023L) ) detectar_discordancias(d, senal_redundante(c("anio_fiscal", "anio_archivo")))d <- data.frame( anio_fiscal = c(2023L, 2023L, 2022L), anio_archivo = c(2023L, 2023L, 2023L) ) detectar_discordancias(d, senal_redundante(c("anio_fiscal", "anio_archivo")))
Compara filas seleccionadas de una tabla con stringdist y devuelve pares cuya
distancia esta bajo el umbral. El resultado describe similitud, distancia,
medida y alcance; nunca afirma que dos filas representen la misma entidad.
Por omision se combinan como maximo dos columnas de texto o factores, despues
de excluir nombres que parecen identificadores (id, codigo, uuid, entre
otros). Si quedan mas de dos columnas, la funcion pide indicar columnas
explicitamente en vez de mezclar campos que pueden diluir la similitud. La
medida predeterminada es Jaro–Winkler ("jw"), con p = 0.1: Winkler
favorece coincidencias con el mismo prefijo y, por eso, premia una errata al
final mas que una al comienzo. El umbral predeterminado es 0.10. Estos dos
valores cambian respecto de versiones anteriores y pueden cambiar los pares
informados; el umbral se eligio como una precision prudente, no como una
calibracion fina. Ambos argumentos se pueden cambiar.
detectar_duplicados_aproximados( datos, columnas = NULL, metodo = "jw", umbral = 0.1, p = 0.1, muestra = Inf, max_pares = 50000000L, max_resultados = 100L, normalizar = NULL, perfil = NULL, proteger_datos_personales = TRUE, bloque = 1000L, estrategia = "auto", lsh_bandas = 12L, lsh_filas = 3L, lsh_q = 3L, lsh_max_cubeta = 1000L, lsh_muestra_estimacion = 400000L, presupuesto_pares = Inf, bloquear_por = NULL, lotes = FALSE, tamano_lote = 1000L, directorio_lotes = NULL, nucleos = getOption("lupa.nucleos", 2L), max_largo_valor = .MAX_LARGO_VALOR_CASI_DUPLICADOS )detectar_duplicados_aproximados( datos, columnas = NULL, metodo = "jw", umbral = 0.1, p = 0.1, muestra = Inf, max_pares = 50000000L, max_resultados = 100L, normalizar = NULL, perfil = NULL, proteger_datos_personales = TRUE, bloque = 1000L, estrategia = "auto", lsh_bandas = 12L, lsh_filas = 3L, lsh_q = 3L, lsh_max_cubeta = 1000L, lsh_muestra_estimacion = 400000L, presupuesto_pares = Inf, bloquear_por = NULL, lotes = FALSE, tamano_lote = 1000L, directorio_lotes = NULL, nucleos = getOption("lupa.nucleos", 2L), max_largo_valor = .MAX_LARGO_VALOR_CASI_DUPLICADOS )
datos |
Tabla con una fila por entidad observada. |
columnas |
Columnas atomicas a combinar. Cómo se concatena, para poder rehacer la distancia. Los valores se unen
con escapar <- function(v) {
gsub("|", "\\|", gsub("\\", "\\\\", v, fixed = TRUE), fixed = TRUE)
}
fila <- function(i) paste(escapar(datos$c1[i]), escapar(datos$c2[i]), sep = " | ")
stringdist::stringdist(fila(1), fila(2), method = "jw", p = 0.1)
Fuera de esos dos escapes, el texto corriente se compara tal cual. Un valor
con marca Si dos columnas de texto tienen el mismo nombre, la comparación no corre:
identifica las columnas por nombre y no podría decir cuál compara. No se
compara un subconjunto —sacar una columna cambia el significado de la
comparación—: el objeto sale sin pares, con el motivo en |
metodo |
Medida admitida por |
umbral |
Distancia maxima para informar un par. Por defecto |
p |
Factor de prefijo de Jaro–Winkler, entre 0 y 0.25. Por defecto
|
muestra |
Máximo de filas candidatas. En el camino exacto queda sujeto
a |
max_pares |
Máximo de pares comparados en el camino exacto. Por defecto
|
max_resultados |
Maximo de pares devueltos. Por defecto |
normalizar |
Perfil de comparación. Hasta dónde llega esa descomposición. La tabla que usa |
perfil |
Perfil de los mismos datos para reutilizar su clasificacion de datos personales y no volver a inferirla. |
proteger_datos_personales |
Si la evidencia de columnas protegidas se
reemplaza por |
bloque |
Cantidad de filas por tesela de comparación. Por defecto
|
estrategia |
Estrategia de comparación: |
lsh_bandas |
Número de bandas del esquema LSH. Por defecto, 12. |
lsh_filas |
Número de filas de firma por banda. Por defecto, 3. |
lsh_q |
Longitud de los q-gramas usados para MinHash. Por defecto, 3. |
lsh_max_cubeta |
Umbral a partir del cual una cubeta se considera grande y se procesa por el mismo troceo acotado del camino exhaustivo. No se descartan pares por este umbral; el alcance informa cuántas cubetas y cuántos pares se procesaron de esta forma. Por defecto, 1000. |
lsh_muestra_estimacion |
Cantidad máxima de pares de filas usados para
estimar la proporción de candidatos, el tiempo del camino LSH y, si hay
|
presupuesto_pares |
Presupuesto de pares candidatos. Por
defecto es |
bloquear_por |
Nombre de una columna declarada por el usuario para
restringir la comparación a filas con la misma clave. La clave no tiene
significado incorporado en |
lotes |
Si es |
tamano_lote |
Cantidad de filas por grupo de trabajo cuando |
directorio_lotes |
Directorio base elegido por el usuario para los
parciales. Si es |
nucleos |
Cantidad máxima de hilos que |
max_largo_valor |
Maximo de caracteres permitido en cada valor de las
columnas combinadas, medido sobre la cadena que de verdad se compara:
las columnas ya unidas y ya normalizadas. Las dos mitades importan. Dos
columnas de 9.000 caracteres estan las dos por debajo de un tope de 10.000 y
llegan a 18.003 una vez unidas; y la normalizacion |
tipo_par = "exacto" indica que los textos guardados son iguales en las
columnas comparadas; una celda ausente (NA) y una cadena vacía cuentan como
el mismo texto vacío, que es como entran a la comparación, así que esa
diferencia no se atribuye a la normalización. Dos dobles son iguales si lo son
sus valores, no su escritura con 15 cifras. tipo_par = "exacto_normalizado" indica que coinciden
despues de la normalizacion declarada, aunque los textos guardados difieran.
Los restantes son "aproximado". La columna logica igualo_normalizar es
TRUE unicamente para el segundo caso. La clasificacion no depende de que
una medida de distancia devuelva cero: por ejemplo, soundex puede dar
distancia cero para textos distintos. Ningun par demuestra identidad.
Como la normalizacion por omision iguala mayusculas, espacios, acentos y
comillas, el objeto conserva esta distincion tanto en pares como en los
hallazgos. duplicados_exactos_normalizados es un tipo de hallazgo propio:
no afirma que los valores guardados sean iguales y tampoco oculta la causa
de la coincidencia bajo duplicados_aproximados.
Una fila cuyos valores en las columnas comparadas son todos ausentes,
cadena vacía o solo blancos —espacios, tabulaciones, saltos de línea; lo que
perfilar() cuenta en n_blancos— no entra en la comparación, con o sin
normalización: no hay texto que medir, y dos filas
vacías no son un duplicado sino dos ausencias. alcance$n_filas_validas
cuenta las que sí entraron —contra n_filas_total— y, si no quedan dos,
razon lo dice. Una cadena vacía sí es una clave válida en
bloquear_por: ahí no se compara su texto, se agrupa por él.
En alcance, n_pares_exactos cuenta solo textos guardados iguales,
n_pares_exactos_normalizados cuenta coincidencias producidas por la
normalizacion y n_pares_aproximados cuenta los pares restantes. Son
conteos disjuntos y explican n_pares_hallados.
La comparacion usa teselas de bloque filas: cada matriz temporal se
descarta antes de continuar, por lo que la memoria no crece con el tamaño
de la tabla. El recorrido es exacto para las filas seleccionadas. muestra
y max_pares recortan el camino exacto; en el camino LSH, muestra = Inf
conserva todas las filas y max_pares no se aplica a las comparaciones.
En ese camino alcance$limite_pares es NA, mientras
limite_pares_configurado conserva el valor pedido como referencia y
limite_pares_aplica permite distinguir ambos casos sin inferirlo.
El objeto informa cuantos pares eran posibles, cuantos se compararon, el
modo, el tamaño de las teselas o los parámetros LSH y los que quedaron fuera. Solo se muestran
max_resultados coincidencias; el truncamiento tambien queda declarado.
El recorte conserva los pares mas cercanos y, entre los empatados en distancia, desempata por el rango canonico del valor con una clave simetrica. Es la misma regla que gobierna el recorte del vocabulario, y existe por lo mismo: desempatar por posicion de fila hacia que cuales pares sobrevivieran dependiera de como viniera ordenado el archivo. Medido sobre 60 pares empatados con el corte en 30, cinco ordenes distintos devolvian 30 grupos cada uno sin compartir ninguno; ahora devuelven los mismos.
Lo que ningun orden evita es que un corte dentro de un empate deje afuera pares
igual de cercanos, asi que se declara. alcance publica distancia_corte —la
distancia donde cayo el corte—, n_en_distancia_corte —cuantos de los
conservados la comparten— y corte_en_empate, que no es truncado con
otro nombre: vale FALSE cuando el corte cae en una distancia unica. Si vale
TRUE, subir max_resultados por encima del empate devuelve todos los pares
de esa distancia.
Queda un limite que ningun orden puede sacar: si varias filas tienen el mismo valor en la columna comparada, sus pares empatan tambien en el rango, y ahi el desempate cae en la posicion. Medido: con cuatro filas y solo dos valores distintos, el conjunto de pares de valores es identico en los cinco ordenes, pero cuales instancias de fila los representan cambia. Es irreducible: dos filas con el mismo valor son indistinguibles en esa columna, y su unica identidad es la posicion, que es justamente lo que varia al reordenar. Si importa que instancia se informa, hace falta una clave que las distinga.
En el camino LSH el conjunto de candidatos depende del orden de las filas,
porque el vocabulario de q-gramas se numera por orden de primera aparicion y esa
numeracion alimenta las firmas. Eso ocurre dentro de la garantia declarada en
lsh_garantia_jaccard_*: medido sobre 1.200 filas, de los pares que cambian al
barajar ninguno supera un Jaccard de q-gramas de 0,8, donde el recall declarado
es 0,9998. Conviene saberlo antes de comparar dos corridas sobre el mismo
contenido exportado en distinto orden.
stringdist es una dependencia opcional. Si no esta instalado, la funcion
devuelve un objeto con disponible = FALSE, una tabla vacia y el motivo
explicito; no falla ni presenta silencio como si se hubieran comparado todos
los pares. En ese caso —y cuando la comparación no corre por un valor que
supera max_largo_valor, por dos columnas con el mismo nombre o por no haber
columnas comparables— los conteos de alcance que salen de comparar filas
(n_filas_validas, n_pares_hallados, n_pares_exactos,
n_pares_exactos_normalizados y n_pares_aproximados) son NA, no cero:
nadie los midió. Si las filas se recorrieron y no quedaron dos válidas, cero
pares sí es lo medido.
Las comparaciones que delegan en stringdist usan nucleos hilos como
máximo. El valor por omisión es getOption("lupa.nucleos", 2L), se limita a
los núcleos disponibles y se publica como alcance$nucleos_usados. Cambiar
la cantidad de hilos no cambia los pares ni los hallazgos, aunque sí puede
cambiar el tiempo de ejecución.
p es el factor de prefijo de Jaro–Winkler y admite valores entre 0 y
0.25; sólo tiene efecto cuando metodo = "jw". Para los demás métodos se
valida pero stringdist no lo utiliza.
El aviso interactivo señala esta perilla: subir nucleos puede acortar la
etapa de comparación, pero la ganancia depende de la máquina y de los datos.
Para tablas que superan el tope exhaustivo, estrategia = "auto" usa
MinHash con bandas LSH sobre todas las filas cuando muestra = Inf. La
salida declara las bandas, las filas por banda, las cubetas descartadas y la
probabilidad teorica de colision. Esa probabilidad se refiere al Jaccard de
los q-gramas, no a la medida final (metodo). estrategia = "lsh" fuerza
este camino; "teselas" y "muestra" conservan el camino exacto o
muestreado de las versiones anteriores. Las cubetas que superan
lsh_max_cubeta se procesan igualmente por lotes; el parámetro identifica
cubetas potencialmente costosas, pero no descarta sus pares. El alcance
separa lsh_teselas_cubetas_grandes (matrices del primer tramo) de
lsh_lotes_cubetas_grandes (lotes de filas de las bandas posteriores), pues
no son la misma unidad. Los pares aceptados por ambos recorridos entran al
resumen de Jaccard. Si alguna implementación futura descarta una cubeta,
la garantía se devuelve como NA y lsh_garantia_estado lo deja explícito.
La familia MinHash usa una permutación aleatoria inyectiva del vocabulario,
con una semilla interna fija, y restaura el estado global del generador de R;
por eso es determinista sin depender de set.seed() ni de RNGkind(). La
tabla de consulta de cada hash evita repetir trabajo para cada celda de la
matriz de q-gramas y hace que el resultado no dependa de cómo se numeraron
esos q-gramas. Antes de recorrer las bandas se toma una muestra interna y se
publica una estimación reproducible de candidatos. El cronómetro de la medida
se ejecuta durante al menos 50 ms y queda en resultado$estimacion, separado
de alcance y marcado como no determinista. Es un piso de la medida aislada
y no estima la firma, las cubetas ni el troceo; sólo se muestra como aviso en
una sesión interactiva.
Fuera de una sesión interactiva se señala una condición silenciosa de clase
lupa_tiempo_lsh, sólo en el camino LSH: no se imprime en stdout ni
stderr. Se puede capturar con
withCallingHandlers(resultado <- detectar_duplicados_aproximados(...), lupa_tiempo_lsh = function(c) { ... }). Como hereda de message, un
tryCatch(..., message = ...) puede interrumpir la corrida y devolver el
valor del manejador; para observar sin interrumpir use withCallingHandlers
y, si corresponde, invokeRestart("muffleMessage").
presupuesto_pares permite rechazar el recorrido antes de iniciarlo; sólo se
pregunta en una sesión interactiva. El diagnóstico de
Jaccard puede quedar limitado a los primeros
pares del recorrido determinista; lsh_jaccard_alcance lo dice literalmente
y no presenta ese prefijo como una muestra representativa.
Con bloquear_por, alcance separa los pares estructuralmente fuera del
bloqueo de una estimación de los candidatos que se habrían informado y que
quedaron fuera según una muestra determinista. La segunda cantidad es una
estimación, no una cuenta exacta.
Lista de clase duplicados_aproximados con pares, hallazgos,
alcance, columnas, metodo, umbral, disponible, razon y
estimacion. alcance es reproducible; en el camino LSH,
estimacion$tiempo_determinista es FALSE y reúne la velocidad, duración
y tiempo de referencia medidos en esa corrida. El campo
estimacion$tiempo_estimado_etapa indica que ese piso cubre sólo la
comparación stringdist, no la firma, las cubetas ni el troceo. En el camino exacto o cuando
no se puede comparar, estimacion es NULL. Si lotes = TRUE, se agrega
lotes con el directorio, los archivos RDS, sus tamaños, el estado de
completitud y reanudable = FALSE. El loteo cruza todos los grupos, por lo
que no pierde pares; su resultado de pares, hallazgos y alcance es el
mismo que el recorrido exacto sin lotes. Cada fila de hallazgos declara
n_evaluados, n_afectados y unidad_conteo; los conteos desconocidos
son NA, nunca cero.
Broder, A. Z. (1997) doi:10.1109/SEQUEN.1997.666900. On the resemblance and containment of documents. En Compression and Complexity of Sequences, 21–29. Leskovec, J., Rajaraman, A. y Ullman, J. D. (2020) doi:10.1017/9781108684163. Mining of Massive Datasets (3.ª ed.), capítulo 3.
perfilar(), reportar(), planificar_limpieza()
datos <- data.frame( nombre = c("Ana Perez", "Ana Peres", "Luis Diaz"), domicilio = c("Calle 1", "Calle 1", "Calle 9") ) pares <- detectar_duplicados_aproximados(datos) if (!pares$disponible) pares$razondatos <- data.frame( nombre = c("Ana Perez", "Ana Peres", "Luis Diaz"), domicilio = c("Calle 1", "Calle 1", "Calle 9") ) pares <- detectar_duplicados_aproximados(datos) if (!pares$disponible) pares$razon
Reconoce formatos de fecha y fecha-hora sin escoger arbitrariamente entre
día/mes y mes/día. Cuando todos los valores con barra, guion o punto son
ambiguos, devuelve todas sus lecturas posibles con estado "candidato":
día/mes y mes/día y, con guion y dos dígitos en cada parte (01-02-03),
también año/mes/día. Esas lecturas son una sola representación. El atributo
formatos_mixtos
indica si hay evidencia de dos o más representaciones en la columna.
El nombre May es a la vez el mes completo en inglés y la abreviatura de
mayo: se cuenta como %B o %b según cómo escriba el mes el resto de la
columna, y no como una representación aparte.
Se aceptan días y meses con uno o dos dígitos. Los años de dos dígitos se
detectan, pero siempre quedan como candidatos y se señalan en la columna
anio_dos_digitos: el siglo no se interpreta en silencio.
También reconoce meses escritos en español rioplatense (setiembre y
set) y en inglés, con la estructura completa de una fecha o como mes y
año. La tabla interna de nombres no usa LC_TIME, por lo que el resultado
es independiente del locale del proceso; los nombres de mes dentro de una
oración no se reconocen. Un mes escrito desambigua día/mes, pero un año de
dos dígitos sigue siendo candidato. Los formatos escritos de mes y año,
igual que las fechas compactas, exigen un año entre 1800 y 2100.
El formato compacto %Y%m%d exige un año entre 1800 y 2100 para evitar que
identificadores de ocho dígitos se clasifiquen parcialmente como fechas.
Una hora con segundo 60 no se acepta: R la leería como el minuto siguiente.
Tampoco la hora 24:00: R la leería como la medianoche del día siguiente,
y el perfil publicaría una fecha que nadie escribió.
detectar_formatos_fecha(x, muestra = 1e+05)detectar_formatos_fecha(x, muestra = 1e+05)
x |
Vector de texto, fechas o fechas-hora. |
muestra |
Máximo de valores que se analizan. |
Data frame con formato, frecuencia, proporción, estado, granularidad
("dia" o "mes") y conteos de casos inequívocos y ambiguos. Los
atributos informan el muestreo, la cantidad de valores compatibles y la
presencia de formatos mixtos.
detectar_formatos_fecha(c("2020-01-31", "31/01/2020")) detectar_formatos_fecha(c("01/02/2020", "02/03/2020"))detectar_formatos_fecha(c("2020-01-31", "31/01/2020")) detectar_formatos_fecha(c("01/02/2020", "02/03/2020"))
Examina los pares de columnas declarados y describe su cardinalidad a partir
de la unicidad completa de cada lado. La cobertura tabla1_en_tabla2 es la
proporción de valores no ausentes de la primera columna que existe en la
segunda; la cobertura inversa se informa de forma simétrica. Así se puede
escoger la dirección PK/FK sin imponerla de antemano.
detectar_relaciones( tabla1, tabla2, muestra = 1e+05, columnas_candidatas = NULL, umbral_cobertura = 0.9, podar = FALSE, tope_memoria_mb = Inf, .rangos = NULL )detectar_relaciones( tabla1, tabla2, muestra = 1e+05, columnas_candidatas = NULL, umbral_cobertura = 0.9, podar = FALSE, tope_memoria_mb = Inf, .rangos = NULL )
tabla1, tabla2
|
Objetos que heredan de |
muestra |
Máximo de filas del lado verificado que se usan para estimar
cada cobertura. El muestreo es sistemático y reproducible; el lado de
referencia no se muestrea. Use |
columnas_candidatas |
Lista de dos vectores de nombres, para |
umbral_cobertura |
Umbral usado por la poda de cardinalidades imposibles. |
podar |
Si se aplican las podas que cambiarían lo informado —tipos
incompatibles y cardinalidades imposibles—. |
tope_memoria_mb |
Presupuesto de memoria para las filas comparadas, en
megabytes. Las combinaciones pendientes se declaran como podas cuando se
alcanza; |
.rangos |
Uso interno de |
Los valores se comparan por su identidad. Los números, por su valor; los
instantes y las fechas también, con su fracción: dos instantes del mismo
segundo no coinciden, y un instante sin fracción se escribe al segundo
—2020-01-01 10:00:00— para que un texto con esa escritura lo encuentre.
Un texto con bytes que no son UTF-8 válido se compara por sus bytes, como
%in%: no es un ausente, y dos valores así distintos no coinciden.
columnas_candidatas permite evitar la exploración de columnas que el usuario
sabe que no pueden participar. El costo crece con el producto de anchos: dos
tablas de treinta columnas son novecientas combinaciones por par de tablas, y
declarar cuáles pueden participar es lo que lo vuelve manejable.
Hay dos clases de poda y el paquete no las trata igual. Dos columnas de
la misma familia con rangos numéricos disjuntos, medidos sobre la columna
completa o sobre el universo DBI, no comparten ningún valor, y eso se sabe sin
comparar: la fila sale como siempre —sin_coincidencias, con cobertura
cero— y la comparación se ahorra. Esa poda está siempre activa porque no
cambia lo que el objeto informa.
Las otras dos sí lo cambiarían. Familias distintas parece decisivo y no lo
es: una columna de texto puede guardar "2020-01-05" y coincidir con una de
fecha. Y una cardinalidad imposible no dice que no haya coincidencias, dice
que no alcanzan umbral_cobertura, que es otra cosa. La cota es por filas,
como la cobertura: con k valores distintos en tabla1, las filas cubiertas
de tabla2 no superan las de sus k valores más frecuentes. Por eso van detrás de
podar = TRUE, y cuando se aplican el par no desaparece: sale con
cardinalidad = "sin_comparar", coberturas NA y su motivo en motivo_poda.
Un par que no se evaluó no es un par sin relación.
Dos pares no se comparan nunca, con podar o sin él, porque la comparación
no daría una respuesta sino una falsa. Si una tabla tiene dos columnas con el
mismo nombre, el nombre no dice cuál de las dos es: sus pares salen
sin_comparar con motivo_poda = "nombre_repetido". Y una fecha contra una
fecha-hora no comparte escritura —2020-01-01 contra 2020-01-01 00:00:00—, así
que ningún valor coincidiría aunque fueran los mismos días: el par sale con
motivo_poda = "fecha_contra_instante". Convertir una de las dos a la clase de
la otra, con tz explícito, y volver a comparar.
Tampoco entra una columna que no tiene un valor comparable por fila: una
compuesta —matriz, tabla anidada, valores que no se escriben como texto,
como una vctrs_rcrd—, una lista de objetos —de vectores, de geometrías,
de NULL— o una magnitud con unidad (units), cuyo número sin la unidad
no es su valor. Sus pares salen sin_comparar con
motivo_poda = "columna_no_comparable", y el detalle en podas dice cuál
de las tres es; la función sigue con las demás columnas.
Todas las podas, de las dos clases, quedan además en el atributo podas con
su motivo y su detalle.
Cuando una tabla supera muestra, la función estima cada cobertura con una
muestra sistemática del lado que se verifica y conserva completo el conjunto
de referencia. La cardinalidad y la cantidad de valores comunes siempre se
calculan con ambas columnas completas.
Data frame con columna_tabla1, columna_tabla2, cardinalidad
—1:1, 1:m, m:1, m:m, sin_coincidencias o sin_comparar—,
n_valores_comunes,
coberturas de integridad referencial en ambas direcciones y motivo_poda,
que sólo tiene valor en los pares no comparados. Los atributos
filas_totales, filas_analizadas y muestreado documentan el muestreo;
podas, n_pares_totales, n_pares_comparados y n_pares_podados
documentan qué se comparó y qué no.
detectar_claves(), referencial(), proponer_modelo()
personas <- data.frame(id = 1:3) tramites <- data.frame(persona_id = c(1, 1, 3, 4)) detectar_relaciones(personas, tramites)personas <- data.frame(id = 1:3) tramites <- data.frame(persona_id = c(1, 1, 3, 4)) detectar_relaciones(personas, tramites)
Resume frecuencias sin conservar una tabla completa de alta cardinalidad.
Cada columna se limita a max_valores; alcance declara cuántos valores se
analizaron, cuántos distintos se observaron y si hubo muestreo o truncamiento.
Los cuantiles se calculan sólo para números ordinarios finitos.
distribucion_valores( datos, perfil = NULL, max_valores = 20L, probabilidades = c(0, 0.25, 0.5, 0.75, 1), muestra = 1e+05, proteger_datos_personales = TRUE, columnas_personales = character(), .universo = NULL )distribucion_valores( datos, perfil = NULL, max_valores = 20L, probabilidades = c(0, 0.25, 0.5, 0.75, 1), muestra = 1e+05, proteger_datos_personales = TRUE, columnas_personales = character(), .universo = NULL )
datos |
Tabla que se desea examinar. |
perfil |
Perfil opcional de los mismos datos; evita repetir la
clasificación de posibles datos personales. Si el perfil declaró
|
max_valores |
Máximo de valores mostrados por columna. |
probabilidades |
Probabilidades de los cuantiles, en |
muestra |
Máximo de filas por columna; |
proteger_datos_personales |
Si se ocultan valores de columnas cuya
clasificación activa protección automática. Véase |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
.universo |
Uso interno de |
Las proporciones de frecuencias se calculan sobre los valores que se
pudieron analizar, no sobre la columna entera, y alcance dice cuántos
fueron: n_total contra n_analizados. Cuando alguno no se pudo comparar
—bytes inválidos, sobre todo— el estado de esa columna es
calculada_parcial en vez de calculada, para que un resultado parcial no
se lea como completo. Un NA no cuenta como descarte: es una ausencia
declarada, no un valor que no se pudo analizar. Los otros estados posibles
son sin_valores, sin_valores_analizables y tipo_no_comparable. La
entrada es la de perfilar(): un POSIXlt se cuenta por su instante, como
el POSIXct equivalente, y el nivel NA de un factor es un faltante.
Cuando una columna tiene evidencia suficiente para activar la protección de
datos personales, sus frecuencias y niveles se conservan pero el valor
concreto se reemplaza. Los cuantiles
mantienen sus filas y probabilidades, pero valor queda en NA y estado
informa "valor_protegido": un cuantil, especialmente en tablas pequeñas,
puede coincidir exactamente con una observación. Esta protección es
independiente de la usada al construir el perfil.
Hay clases que R declara numéricas y cuyo cuantil interpolado no se puede
calcular, porque su aritmética rechaza el producto por una fracción
—lubridate::Period es una—. Esas columnas no interrumpen el análisis:
conservan sus filas con valor en NA y el estado
"no_interpolable:<clase>", que nombra la clase que no se pudo interpolar. La
condición se mide intentando el cálculo, no enumerando clases, así que una clase
nueva con la misma limitación queda cubierta sin tocar el código.
Las frecuencias se cuentan por valor, no por su escritura: dos dobles
que coinciden en sus primeras 15 cifras —0.1 + 0.2 y 0.3, 1e15 y
1e15 + 1— son dos filas, como los cuenta perfilar(), y cada una se
escribe con las cifras que la devuelven a su valor. Un instante se escribe
en UTC y con el rótulo UTC, como los resúmenes de perfilar(): la
etiqueta no depende de la zona de la sesión. A igual frecuencia, las
filas se ordenan por valor —el texto por sus bytes, los números como
números—, el mismo desempate que la moda de perfilar(): así, el recorte de
max_valores no depende del orden de las filas de la tabla.
Objeto distribuciones_perfil, una lista con data frames
frecuencias, cuantiles y alcance. Todas las proporciones están en
[0, 1].
perfilar(), analizar(), clasificar_variables()
d <- data.frame(grupo = c("A", "A", "B"), valor = c(1, 2, 10)) distribucion_valores(d)d <- data.frame(grupo = c("A", "A", "B"), valor = c(1, 2, 10)) distribucion_valores(d)
Muestra las candidatas de sugerir_clave() numeradas, con el motivo de cada
una, y devuelve la que se elija para pasarla al argumento clave de
perfilar(). La última opción permite escribir un nombre que no esté en la
lista, o varios separados por coma para una clave compuesta.
elegir_clave(datos, maximo = 5L, umbral_casi = 0.95)elegir_clave(datos, maximo = 5L, umbral_casi = 0.95)
datos |
Tabla a examinar. |
maximo |
Cuántas sugerencias devolver como máximo: un entero positivo. |
umbral_casi |
Proporción de valores distintos a partir de la cual una columna que repite se informa igual, como candidata con duplicados. |
En una sesión no interactiva no pregunta: devuelve NULL y avisa qué
habría ofrecido. Un guion que corre solo no puede quedarse esperando una
respuesta, y elegir por su cuenta sería exactamente lo que esta función
existe para no hacer.
El nombre —o los nombres— de la clave elegida, o NULL si no se
eligió ninguna o la sesión no es interactiva.
sugerir_clave(), y el argumento clave de perfilar().
personas <- data.frame(id_persona = 1:3, edad = c(30, 41, 25)) # En una sesion interactiva pregunta; aqui devuelve NULL y dice que ofreceria. elegir_clave(personas)personas <- data.frame(id_persona = 1:3, edad = c(30, 41, 25)) # En una sesion interactiva pregunta; aqui devuelve NULL y dice que ofreceria. elegir_clave(personas)
Los siete estadísticos que medicion_desde_estimaciones() sabe llevar al
contrato de medir(), con la métrica a la que corresponden, su tipo, su
unidad y su orientación: si un valor alto es conformidad o defecto. Sin
la orientación el número no se puede evaluar, porque un coeficiente de
variación de 0,30 y un tamaño de muestra de 0,30 no se leen igual.
estadisticos_estimacion()estadisticos_estimacion()
Data frame con estadistico, metrica, tipo_resultado,
orientacion y unidad.
medicion_desde_estimaciones(), evaluar()
estadisticos_estimacion()estadisticos_estimacion()
Construye las firmas y calcula el pronóstico de candidatos sin recorrer las
cubetas ni comparar los pares. Es una operación deliberada: la medición del
reloj queda en el resultado de esta función y no es necesaria para obtener
alcance reproducible en detectar_duplicados_aproximados().
estimar_costo( datos, columnas = NULL, metodo = "jw", umbral = 0.1, p = 0.1, muestra = Inf, max_pares = 50000000L, max_resultados = 100L, normalizar = NULL, perfil = NULL, proteger_datos_personales = TRUE, bloque = 1000L, estrategia = "auto", lsh_bandas = 12L, lsh_filas = 3L, lsh_q = 3L, lsh_max_cubeta = 1000L, lsh_muestra_estimacion = 400000L, presupuesto_pares = Inf, bloquear_por = NULL, lotes = FALSE, tamano_lote = 1000L, directorio_lotes = NULL, nucleos = getOption("lupa.nucleos", 2L), max_largo_valor = .MAX_LARGO_VALOR_CASI_DUPLICADOS )estimar_costo( datos, columnas = NULL, metodo = "jw", umbral = 0.1, p = 0.1, muestra = Inf, max_pares = 50000000L, max_resultados = 100L, normalizar = NULL, perfil = NULL, proteger_datos_personales = TRUE, bloque = 1000L, estrategia = "auto", lsh_bandas = 12L, lsh_filas = 3L, lsh_q = 3L, lsh_max_cubeta = 1000L, lsh_muestra_estimacion = 400000L, presupuesto_pares = Inf, bloquear_por = NULL, lotes = FALSE, tamano_lote = 1000L, directorio_lotes = NULL, nucleos = getOption("lupa.nucleos", 2L), max_largo_valor = .MAX_LARGO_VALOR_CASI_DUPLICADOS )
datos |
Tabla con una fila por entidad observada. |
columnas |
Columnas atomicas a combinar. Cómo se concatena, para poder rehacer la distancia. Los valores se unen
con escapar <- function(v) {
gsub("|", "\\|", gsub("\\", "\\\\", v, fixed = TRUE), fixed = TRUE)
}
fila <- function(i) paste(escapar(datos$c1[i]), escapar(datos$c2[i]), sep = " | ")
stringdist::stringdist(fila(1), fila(2), method = "jw", p = 0.1)
Fuera de esos dos escapes, el texto corriente se compara tal cual. Un valor
con marca Si dos columnas de texto tienen el mismo nombre, la comparación no corre:
identifica las columnas por nombre y no podría decir cuál compara. No se
compara un subconjunto —sacar una columna cambia el significado de la
comparación—: el objeto sale sin pares, con el motivo en |
metodo |
Medida admitida por |
umbral |
Distancia maxima para informar un par. Por defecto |
p |
Factor de prefijo de Jaro–Winkler, entre 0 y 0.25. Por defecto
|
muestra |
Máximo de filas candidatas. En el camino exacto queda sujeto
a |
max_pares |
Máximo de pares comparados en el camino exacto. Por defecto
|
max_resultados |
Maximo de pares devueltos. Por defecto |
normalizar |
Perfil de comparación. Hasta dónde llega esa descomposición. La tabla que usa |
perfil |
Perfil de los mismos datos para reutilizar su clasificacion de datos personales y no volver a inferirla. |
proteger_datos_personales |
Si la evidencia de columnas protegidas se
reemplaza por |
bloque |
Cantidad de filas por tesela de comparación. Por defecto
|
estrategia |
Estrategia de comparación: |
lsh_bandas |
Número de bandas del esquema LSH. Por defecto, 12. |
lsh_filas |
Número de filas de firma por banda. Por defecto, 3. |
lsh_q |
Longitud de los q-gramas usados para MinHash. Por defecto, 3. |
lsh_max_cubeta |
Umbral a partir del cual una cubeta se considera grande y se procesa por el mismo troceo acotado del camino exhaustivo. No se descartan pares por este umbral; el alcance informa cuántas cubetas y cuántos pares se procesaron de esta forma. Por defecto, 1000. |
lsh_muestra_estimacion |
Cantidad máxima de pares de filas usados para
estimar la proporción de candidatos, el tiempo del camino LSH y, si hay
|
presupuesto_pares |
Se acepta por simetría de la firma y no aborta
aquí: estimar el costo es justamente lo que se hace antes de fijar un
presupuesto, así que interrumpir la estimación por superarlo dejaría sin
respuesta la pregunta que motivó la llamada. Quien aborta es
|
bloquear_por |
Nombre de una columna declarada por el usuario para
restringir la comparación a filas con la misma clave. La clave no tiene
significado incorporado en |
lotes |
Se acepta por simetría de la firma, pero la estimación no escribe parciales ni modifica el directorio indicado. |
tamano_lote |
Se acepta por simetría; no cambia el pronóstico. |
directorio_lotes |
Se acepta por simetría y no se crea ni se usa al estimar. |
nucleos |
Cantidad máxima de hilos que |
max_largo_valor |
Maximo de caracteres permitido en cada valor de las
columnas combinadas, medido sobre la cadena que de verdad se compara:
las columnas ya unidas y ya normalizadas. Las dos mitades importan. Dos
columnas de 9.000 caracteres estan las dos por debajo de un tope de 10.000 y
llegan a 18.003 una vez unidas; y la normalizacion |
En el camino LSH, candidatos_previstos es una estimación reproducible a
partir de una muestra de firmas. tiempo_estimado_segundos es un piso de la
medida aislada y no incluye firmas, cubetas ni troceo; sus campos de reloj
tienen tiempo_determinista = FALSE. Con bloquear_por, los pares entre
bloques no entran en el pronóstico y la pérdida estructural y estimada queda
en alcance. En el camino exacto, candidatos_previstos es la cantidad de
pares que se compararán, sujeta a muestra, max_pares y bloquear_por.
La función no escribe archivos ni modifica el estado del generador de R.
Lista de clase estimacion_costo_lupa con los campos de la
estimación, alcance, disponible y razon. En el camino exacto,
alcance declara además la corrida que se está estimando:
modo_comparacion es el mismo que publicará
detectar_duplicados_aproximados() con esos argumentos —exhaustiva_por_bloques
o muestreada_por_bloques—, y filas_previstas, filas_totales y
estrategia_prevista dicen sobre cuántas filas se va a comparar y con qué
selección. Una estimación que anunciara un recorrido exhaustivo sobre una
corrida que va a muestrear no serviría para decidir.
detectar_duplicados_aproximados()
datos <- data.frame( nombre = c("Ana Perez", "Ana Peres", "Luis Diaz"), grupo = c("A", "A", "B") ) if (requireNamespace("stringdist", quietly = TRUE)) { costo <- estimar_costo(datos, columnas = "nombre", estrategia = "lsh") costo$candidatos_previstos }datos <- data.frame( nombre = c("Ana Perez", "Ana Peres", "Luis Diaz"), grupo = c("A", "A", "B") ) if (requireNamespace("stringdist", quietly = TRUE)) { costo <- estimar_costo(datos, columnas = "nombre", estrategia = "lsh") costo$candidatos_previstos }
Cuenta cuántas comparaciones de columnas implicaría buscar claves foráneas entre los pares indicados, antes de hacerlas. El número que importa no es la cantidad de tablas sino la de pares de columnas: dos tablas de cincuenta columnas son dos mil quinientas comparaciones.
estimar_costo_coleccion(coleccion, pares = NULL, columnas_candidatas = NULL)estimar_costo_coleccion(coleccion, pares = NULL, columnas_candidatas = NULL)
coleccion |
Objeto creado por |
pares |
Data frame con columnas |
columnas_candidatas |
Lista nombrada por identificador de tabla. Cada entrada declara las columnas que pueden participar de una relación; las tablas no nombradas conservan todas sus columnas. La declaración reduce tanto la lectura como el número de comparaciones. |
Sin pares estima sobre todos los pares dirigidos de la colección, que es
justamente lo que suele mostrar por qué hay que declararlos. Ese caso no
materializa los pares: la suma de c_i * c_j sobre los pares dirigidos es
sum(c)^2 - sum(c^2), así que el trabajo es lineal en el número de
tablas y no cuadrático. Estimar el costo no puede costar más que medirlo.
Sólo se consulta el ancho de las tablas que los pares nombran, por metadatos
(DBI::dbListFields()), no leyendo datos.
Una tabla cuyo esquema no se puede leer no se suma como cero. Queda
declarada en tablas_sin_esquema y los pares que la involucran se cuentan en
n_pares_sin_estimar: el número devuelto cubre n_pares_estimados, no todos
los pares.
Lista con n_tablas, n_pares_dirigidos, n_pares_declarados,
n_pares_estimados, n_pares_sin_estimar, n_comparaciones_columnas,
n_tablas_sin_esquema, tablas_sin_esquema y alcance. También publica
columnas_candidatas y el alcance real de las comparaciones.
relaciones_coleccion(), coleccion()
if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) DBI::dbWriteTable(con, "visitas", data.frame(persona_id = 1:3, dia = 1:3)) estimar_costo_coleccion( coleccion(con, c("personas", "visitas")), columnas_candidatas = list(personas = "id", visitas = "persona_id") ) DBI::dbDisconnect(con) }if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) DBI::dbWriteTable(con, "visitas", data.frame(persona_id = 1:3, dia = 1:3)) estimar_costo_coleccion( coleccion(con, c("personas", "visitas")), columnas_candidatas = list(personas = "id", visitas = "persona_id") ) DBI::dbDisconnect(con) }
Ejecuta la cadena formal: condición por medida, proporción de medidas que
cumplen cada regla y media aritmética simple, no ponderada, de las reglas
del perfil. Los resultados por regla se conservan en reglas; el resumen de
perfiles no sustituye esa distribución.
evaluar(medicion, perfil)evaluar(medicion, perfil)
medicion |
Primer argumento. Data frame producido por |
perfil |
Segundo argumento. Objeto creado por
|
Objeto evaluacion_calidad con tres data frames filtrables:
medidas, reglas y perfiles. Si alguna regla declara un desenlace,
contiene además desenlaces, un plan que identifica las medidas
incumplidas, el valor medido, el motivo y la regla que lo produjo.
Cuando una métrica no pudo medirse, conserva cobertura_metricas y deja
en NA el resumen afectado, en lugar de tratar la ausencia como éxito;
también sobre un agregado, donde la regla nombra la métrica agregada
—agregada:ratio:NoNulo— y la parte sin medir es una de sus instancias.
La evaluación de un agregado conserva además la cobertura de su frontera
—las tablas o colecciones declaradas que no entraron al número—, la
imprime y la lleva a su informe y a historico_calidad().
Y cuando una regla declara una métrica que la medición no trae —ninguna
medida de ella—, el veredicto cubre menos de lo que la regla dice: eso se
avisa al evaluar y queda en el atributo cobertura_reglas, con la corrida,
la métrica, el motivo y cómo resolverlo: se mira corrida por corrida, así
que una corrida evaluada junto a otras avisa lo mismo que evaluada sola. Si ninguna de las métricas declaradas por
una regla tiene medidas, evaluar() se niega y nombra las solicitadas y las
disponibles.
Conserva además, en atributos, la configuración del modelo, la
aplicabilidad y el perfil de evaluación que produjo el resultado.
nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo) instancia <- instanciar(especifica, "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) regla <- regla_evaluacion("Al menos 90%", function(x) x > 0.9) evaluar(medidas, perfil_evaluacion("Avanzado", regla))nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo) instancia <- instanciar(especifica, "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) regla <- regla_evaluacion("Al menos 90%", function(x) x > 0.9) evaluar(medidas, perfil_evaluacion("Avanzado", regla))
granularidades() declara los diez niveles del marco, y los diez se miden.
La columna implementada significa que se puede instanciar una metrica y
medir ese nivel. La columna agregable es distinta: indica que el grafo
ofrece al menos una transicion de agregacion hacia o desde el nivel. Por eso
conjuntoAtributos puede estar implementado para medir una metrica y a la
vez no ser agregable con las transiciones disponibles; motivo_agregacion
deja esa razon escrita.
Los cuatro de arriba —colección, conjunto de colecciones, organización y
conjunto de organizaciones— sólo cuando el usuario declara la frontera:
qué tablas componen una colección, qué bases un conjunto, qué colecciones una
organización, qué organizaciones un conjunto. lupa no infiere ninguna de las
cuatro, porque ninguna está en los datos.
granularidades() transiciones_granularidad()granularidades() transiciones_granularidad()
Que estén implementadas no obliga a usarlas. Un análisis que no tiene una organización detrás se detiene donde corresponda; los niveles superiores existen para quien los necesita.
transiciones_granularidad() devuelve el grafo dirigido de agregaciones.
La transición instanciaAtributo a instanciaEntidad se incorpora porque
el propio marco la usa aunque no aparezca en su tabla no exhaustiva.
Data frames con niveles o aristas del grafo de granularidad.
granularidades() transiciones_granularidad()granularidades() transiciones_granularidad()
Persiste el data frame versionado mediante RDS de base R. La escritura no reemplaza un archivo existente salvo consentimiento explícito.
guardar_historico(historico, archivo, sobrescribir = FALSE) leer_historico(archivo)guardar_historico(historico, archivo, sobrescribir = FALSE) leer_historico(archivo)
historico |
Objeto creado por |
archivo |
Ruta del archivo RDS. Una ruta que ya es un directorio se rechaza: el archivo va dentro, con su nombre. |
sobrescribir |
Si se permite reemplazar un archivo existente. |
guardar_historico() devuelve invisiblemente la ruta normalizada;
leer_historico() devuelve un historico_calidad validado.
guardar_historico(), detectar_deriva_calidad()
historico_calidad(), comparar_evaluaciones()
archivo <- tempfile(fileext = ".rds") guardar_historico(historico_calidad(), archivo) leer_historico(archivo) unlink(archivo)archivo <- tempfile(fileext = ".rds") guardar_historico(historico_calidad(), archivo) leer_historico(archivo) unlink(archivo)
Recorre los grupos pendientes y aquellos que contienen alternativas
destructivas. Muestra evidencia calculada sobre datos, las estrategias y
sus justificaciones, y devuelve el plan editado sin aplicarlo. En una sesión
no interactiva retorna inmediatamente el plan sin cambios, salvo que se
proporcione un selector explícito.
guiar_limpieza( plan, datos, selector = NULL, diccionarios = list(), max_ejemplos = 5L )guiar_limpieza( plan, datos, selector = NULL, diccionarios = list(), max_ejemplos = 5L )
plan |
Objeto |
datos |
Datos correspondientes al perfil que originó el plan. |
selector |
Función opcional que recibe una lista con |
diccionarios |
Lista opcional con nombre de diccionarios por columna. |
max_ejemplos |
Máximo de ejemplos reales mostrados por grupo. |
No se representa "no hacer nada" como una acción ficticia. Elegirlo cambia
decision_grupo a "omitida"; por contraste, un grupo aún no revisado
conserva "pendiente". Cuando conservar los datos es la recomendación, esa
opción también se muestra con la marca "(Recomendado)" y su justificación.
Los diccionarios de capitalización se suministran como una lista con nombre
de vectores con nombre.
El mismo plan_limpieza recibido, con la columna de decisión
sincronizada según lo elegido, y sin ejecutar ninguna acción sobre los
datos: cambia qué acciones quedan marcadas para aplicar(), no la
tabla. En una sesión no interactiva y sin selector, devuelve el plan
sin cambios: también un plan ya editado, cuyo decision_grupo se
conserva tal como lo recibió. La sincronización de decisiones corre en el
camino interactivo y en aplicar(), no en la puerta de salida.
planificar_limpieza(), aplicar()
datos <- data.frame(zona = c("Norte", "NORTE", "sur")) plan <- planificar_limpieza(perfilar(datos), datos) guiado <- guiar_limpieza(plan, datos) identical(plan, guiado) # TRUE en una sesión no interactivadatos <- data.frame(zona = c("Norte", "NORTE", "sur")) plan <- planificar_limpieza(perfilar(datos), datos) guiado <- guiar_limpieza(plan, datos) identical(plan, guiado) # TRUE en una sesión no interactiva
Crea un data frame plano y versionado con corridas producidas por medir() o
evaluar(). acumular_historico() agrega objetos al mismo esquema y es
idempotente cuando recibe otra vez registros idénticos, incluso si el RDS se
guardó o se vuelve a leer bajo otro locale.
historico_calidad(..., detalle = c("resumen", "completo")) acumular_historico(historico, ..., detalle = c("resumen", "completo"))historico_calidad(..., detalle = c("resumen", "completo")) acumular_historico(historico, ..., detalle = c("resumen", "completo"))
... |
Objetos |
detalle |
Para evaluaciones, |
historico |
Objeto creado por |
El detalle predeterminado evita repetir una fila por celda y regla cuando el
objetivo es monitorear la serie de evaluaciones. El objeto no guarda modelos,
closures, datos originales ni perfiles de profiling. Esto mantiene la tabla
exportable directamente con write.csv() o una herramienta de base de datos.
Al volver a leerla, cada columna recupera el tipo del esquema —read.csv()
lee como lógica una columna de texto que viene entera en NA— y las fechas
de texto se leen en UTC, con su hora y con su desplazamiento cuando lo traen
—2026-01-31 10:00:00-03:00 son las 13:00 UTC; también Z, +0100 o
-03, como los escribe una columna timestamptz de una base—. Un texto que
no se puede leer entero se rechaza: no se lee a medias.
[, subset() y dplyr::filter() conservan la configuración de las
corridas que quedan; rbind() de dos históricos los acumula, como
acumular_historico().
El esquema largo mapea las cuatro tablas de la sección 9.5 del marco mediante
nivel. Las columnas que no corresponden a un nivel quedan como NA.
Data frame S3 historico_calidad. La columna version_esquema y el
atributo del mismo nombre permiten migraciones futuras. nivel corresponde
a medida, evaluacion_medida, evaluacion_regla o
evaluacion_perfil; una métrica sin valores se conserva como
metrica_no_evaluada con su motivo, siempre que la medición tenga al
menos una medida; y una parte declarada en una frontera —una tabla de la
colección, una colección de la organización— que no entró al número, como
parte_no_medida, con la frontera en id_registro, la parte en entidad
y el motivo en objeto_medible —también cuando un objeto reúne varias
corridas con rbind(): cada parte va a la corrida a la que le faltó—. Una medida que una regla declaró desenlace = "suprimir"
no publica su valor tampoco aquí: su fila deja resultado en NA y
marca objeto_medible con [valor suprimido], en los dos niveles donde esa
medida aparece —medida y evaluacion_medida, en este último bajo
cualquier perfil o regla que la evalúe, no solo el que la suprimió—,
porque esta tabla está pensada para exportarse. Con el detalle resumido, las medidas suprimidas
entran igual, enmascaradas en el nivel evaluacion_medida: así una
medición acumulada después —también sobre un histórico guardado o
leído de un CSV— queda tapada en las mismas medidas, sea cual sea el orden
en que llegan los objetos. Una medición enteramente vacía —ninguna métrica
pudo aplicarse— no se acumula: se rechaza citando el motivo que medir()
declaró en cobertura_metricas, porque no hay corrida que registrar. El atributo
configuracion_evaluacion conserva, en una tabla plana separada, el
modelo, su marco y sus tipos, la aplicabilidad, el perfil, la identidad
de tabla y la fecha de cada corrida. Acumular una corrida con un
id_medicion ya presente exige que todo eso coincida, la fecha incluida:
dos entregas distintas son dos corridas, y el error nombra en qué difieren.
Esa misma configuración viaja en las filas evaluacion_perfil, en las
columnas identidad_tabla, configuracion_modelo, configuracion_marco,
configuracion_tipos_resultado, configuracion_aplicabilidad y
configuracion_perfil —en NA en los demás niveles—, porque el atributo no
sobrevive a write.csv(): un histórico releído de un CSV la recupera de
ahí. Son columnas agregadas al esquema 1: un histórico anterior que no las
trae se lee igual, con ellas en NA.
AGESIC (2020). Marco de trabajo para la Gestión de la Calidad de Datos en Gobierno Digital, versión 1.6, sección 9.5, Presidencia de la República, Uruguay.
medir(), evaluar(), detectar_deriva_calidad(), reportar()
historico_calidad(), leer_historico()
nucleo <- metricas_nucleo() instancia <- instanciar(especializar(nucleo$NoNulo), "personas", "edad") medidas <- medir( modelo(instancia), data.frame(edad = c(20, NA)), id_medicion = "enero", fecha = as.POSIXct("2026-01-31", tz = "UTC") ) evaluacion <- evaluar( medidas, perfil_evaluacion("Basico", regla_evaluacion("Presente", function(x) x > 0)) ) historico_calidad(medidas, evaluacion)nucleo <- metricas_nucleo() instancia <- instanciar(especializar(nucleo$NoNulo), "personas", "edad") medidas <- medir( modelo(instancia), data.frame(edad = c(20, NA)), id_medicion = "enero", fecha = as.POSIXct("2026-01-31", tz = "UTC") ) evaluacion <- evaluar( medidas, perfil_evaluacion("Basico", regla_evaluacion("Presente", function(x) x > 0)) ) historico_calidad(medidas, evaluacion)
Sin pesos, devuelve tablero_calidad() y nunca un puntaje. Con pesos
nombrados por dimensión, transforma las métricas de defecto como
1 - valor, excluye las de orientación no_aplica y conserva cada paso.
indice_calidad(medidas, pesos, pesos_internos = NULL, ...)indice_calidad(medidas, pesos, pesos_internos = NULL, ...)
medidas |
Medición, tablero o análisis de |
pesos |
Vector numérico nombrado por dimensión, en |
pesos_internos |
Vector opcional nombrado por |
... |
Argumentos enviados a |
Cuando una dimensión contiene varios componentes, pesos_internos debe
declarar una ponderación completa que sume uno dentro de esa dimensión.
No existe un promedio interno por omisión. El resultado conserva el tablero,
ambas capas de pesos, las inversiones, las exclusiones, los universos y la
cobertura del marco.
Una métrica sin dimensión no entra al índice: se excluye con su motivo, como
las de orientación no_aplica. Un peso para una dimensión cuyas métricas se
excluyeron se rechaza diciéndolo.
advertencia_universos se calcula de los universos que el propio objeto
publica, no es un texto fijo. Con un solo universo lo nombra y dice que las
unidades son comparables; con varios dice cuántos son y nombra, por universo,
qué componentes caen en cada uno; sin componentes no afirma nada sobre
universos. Antes era una sola frase idéntica en toda corrida, que afirmaba
«salen de universos distintos» incluso cuando los tres componentes publicaban
universo = "celdas" —una afirmación que el mismo objeto desmentía en su
columna universo— y que no nombraba nunca ningún componente, que es
justamente lo que prometía declarar.
Sin pesos, un tablero_calidad. Con pesos, un objeto S3
indice_calidad que nunca se imprime como un número aislado.
nucleo <- metricas_nucleo() instancias <- list( instanciar(especializar(nucleo$NoNulo), "padron", "codigo"), instanciar(especializar(nucleo$EntidadDuplicada), "padron") ) medidas <- medir(modelo(instancias), data.frame(codigo = c("A", "B", "B"))) indice_calidad(medidas) # Pesos propios de este ejemplo, no del paquete: indice_calidad( medidas, pesos = c(Completitud = 0.6, Unicidad = 0.4) )nucleo <- metricas_nucleo() instancias <- list( instanciar(especializar(nucleo$NoNulo), "padron", "codigo"), instanciar(especializar(nucleo$EntidadDuplicada), "padron") ) medidas <- medir(modelo(instancias), data.frame(codigo = c("A", "B", "B"))) indice_calidad(medidas) # Pesos propios de este ejemplo, no del paquete: indice_calidad( medidas, pesos = c(Completitud = 0.6, Unicidad = 0.4) )
Para columnas de texto distingue valores lógicos, enteros, dobles, fechas,
fechas-hora, horas, identificadores y texto. Además de la etiqueta devuelve
la proporción de valores compatibles, siempre en la escala [0, 1].
Los textos compuestos únicamente por 0 y 1 se consideran enteros; para
inferir un lógico debe aparecer al menos un literal alfabético como sí,
no, true o false. Una letra suelta (T, F, S, N) cuenta como
literal sólo si aparece también su opuesto —T con F o false, S con
N o no, y al revés—: sola es un código, y una columna de F y M no es
lógica. Los literales se reconocen sin distinguir mayúsculas —SÍ es sí—
con el pliegue de caja de normalizacion(), que no depende del locale.
Un separador seguido de exactamente tres dígitos (1.500, 1,500) cuenta
como número sólo si la columna aporta evidencia de una sola convención
decimal, con punto o con coma por igual.
inferir_tipo(x, umbral = 0.8, muestra = 1e+05)inferir_tipo(x, umbral = 0.8, muestra = 1e+05)
x |
Vector que se desea examinar. Una tabla —también la de una columna
que da |
umbral |
Proporción mínima para asignar un tipo implícito. |
muestra |
Máximo de valores que se analizan. |
Lista de clase inferencia_tipo con tipo, proporcion, conteos,
candidatos evaluados y, cuando corresponde, formatos de fecha. n_analizados
y muestreado declaran cuántos valores sostienen la proporción. El resultado
público no conserva cachés internos del perfilado.
detectar_formatos_fecha(), descubrir_patrones(), perfilar()
inferir_tipo(c("1", "2", "3")) inferir_tipo(c("2020-01-01", "31/12/2020"))inferir_tipo(c("1", "2", "3")) inferir_tipo(c("2020-01-01", "31/12/2020"))
Un marco_calidad enumera las dimensiones y factores contra los que se
interpreta la cobertura de un análisis. No contiene métricas instanciadas:
esa función sigue correspondiendo a modelo(). Tampoco es un catálogo de
métricas; catalogo_agesic() conserva esa correspondencia específica.
marco_calidad(nombre, factores) marco_agesic() marco_iso25012() marco_cepal()marco_calidad(nombre, factores) marco_agesic() marco_iso25012() marco_cepal()
nombre |
Nombre del marco. |
factores |
Data frame con
Las columnas adicionales se conservan como metadatos y no cambian por sí
solas la cobertura. Si se usa una lista, |
marco_agesic() devuelve la taxonomía incluida de fábrica. Un marco propio
puede construirse con un data frame o con una lista cuyos nombres son
dimensiones y cuyos valores son factores. La forma lista aplica a todas las
filas los valores predeterminados descritos abajo; en particular,
perfil_mide = FALSE.
marco_iso25012() adapta las dos perspectivas de ISO/IEC 25012:2008 a la
estructura dimensión-factor. Usa tres grupos disjuntos como dimensiones:
características inherentes, características inherentes y dependientes del
sistema, y características dependientes del sistema. Este agrupamiento es
una representación operativa para lupa, no afirma que la norma defina una
jerarquía dimensión-factor. Los nombres de las quince características y su
clasificación siguen la norma; las descripciones son redacción propia. Las
quince filas declaran perfil_mide = FALSE: a diferencia de la política
incluida para dos factores de AGESIC, el profiling genérico no demuestra por
sí solo que una característica ISO satisfaga el uso declarado.
marco_cepal() representa los cuatro niveles y los diecinueve principios
del marco nacional de aseguramiento de la calidad de las Naciones Unidas,
adoptados y adaptados para América Latina y el Caribe por la CEA/CEPAL. La
columna principio conserva la numeración de la fuente. Los nombres de los
niveles y principios son textuales; las descripciones y las instrucciones
como_resolverlo de este paquete son redacción propia. Los principios 1 a
13 quedan fuera del alcance de una tabla: se refieren al sistema, al entorno
institucional o al proceso estadístico. Los principios 14 a 19 describen
productos estadísticos y quedan disponibles, aunque ninguno se considera
medido por el profiling genérico.
marco_calidad(), marco_agesic(), marco_iso25012() y
marco_cepal() devuelven un objeto S3 marco_calidad.
as.data.frame() devuelve su tabla de factores.
ISO/IEC (2008). ISO/IEC 25012:2008 Software engineering — Software product Quality Requirements and Evaluation (SQuaRE) — Data quality model. https://www.iso.org/standard/35736.html.
Naciones Unidas (2019). Manual del marco nacional de aseguramiento de calidad en las estadísticas oficiales. Estudios en Métodos, serie M, N° 100 (ST/ESA/STAT/SER.M/100), Nueva York.
Grupo de Trabajo de la Conferencia Estadística de las Américas (CEA), coordinado por Colombia (DANE) y México (INEGI), Secretaría Técnica: División de Estadísticas de la CEPAL (2022). Guía para la implementación del marco de aseguramiento de la calidad para procesos y productos estadísticos. LC/CEA.11/19. Comisión Económica para América Latina y el Caribe (CEPAL), Naciones Unidas, Santiago.
catalogo_agesic(), modelo(), cobertura_analisis()
propio <- marco_calidad("Marco operativo", list( Trazabilidad = c("Origen documentado", "Linaje reproducible"), Pertinencia = "Adecuaci\u00f3n al uso" )) propio as.data.frame(propio) marco_agesic() iso <- marco_iso25012() table(as.data.frame(iso)$dimension) marco_cepal()propio <- marco_calidad("Marco operativo", list( Trazabilidad = c("Origen documentado", "Linaje reproducible"), Pertinencia = "Adecuaci\u00f3n al uso" )) propio as.data.frame(propio) marco_agesic() iso <- marco_iso25012() table(as.data.frame(iso)$dimension) marco_cepal()
lupa no estima: eso necesita un diseño muestral, estimación de varianza
y otra disciplina. Lo que sabe hacer es evaluar contra un marco declarado.
Esta función recibe estimaciones calculadas por otra herramienta —survey,
el paquete calidad del INE de Chile,
o cualquier otra— y las convierte en una medición que evaluar() entiende.
medicion_desde_estimaciones( estimaciones, entidad, fuente, atributo = NULL, columnas = NULL, fecha = Sys.time() )medicion_desde_estimaciones( estimaciones, entidad, fuente, atributo = NULL, columnas = NULL, fecha = Sys.time() )
estimaciones |
Data frame con una fila por estimación y una columna por
estadístico. Los nombres reconocidos son los de
|
entidad |
Nombre de la entidad estimada, por ejemplo el tabulado o la población de referencia. |
fuente |
Texto que declara quién calculó las estimaciones. Es obligatorio: sin él, el resultado no dice de dónde viene. |
atributo |
Columna opcional de |
columnas |
Vector con nombres para traducir columnas de |
fecha |
Fecha y hora de la medición. Se lee como en |
Cada estadístico se convierte en una medida canónica propia, con su
métrica, su tipo y su orientación, porque los siete tienen unidades y
dominios distintos: un coeficiente de variación y un tamaño de muestra no se
evalúan con la misma regla. Los reconocidos están en
estadisticos_estimacion().
Cada medida declara su procedencia en fuente, de modo que nadie lea el
resultado como si lupa lo hubiera calculado. Los estadísticos que la tabla
no traiga simplemente no producen medidas: no se rellenan con ceros ni se
estiman.
Una columna vacía tampoco produce medidas. Un estadístico cuya columna
está presente pero no trae ningún valor utilizable no es una estimación: no
se publica con resultado = NA, porque eso sería informar como medido lo que
nadie midió —y además deja la medición inservible, porque evaluar() la
rechaza entera—. Esos estadísticos se declaran en el atributo
estadisticos_sin_valores, separados de estadisticos_ausentes: no es lo
mismo no mandar la columna que mandarla sin datos, y la acción de quien la
preparó es distinta en cada caso. Dentro de una columna que sí trae datos, la
celda vacía se descarta por la misma razón, y el resto de las estimaciones se
publica.
Una celda que no es una estimación también se descarta, y se declara. Un
texto que no es un número, o un número fuera del dominio de su estadístico
—un infinito, en cualquiera; un tamaño de muestra que no es un entero no
negativo; un error estándar, un
coeficiente de variación, unos grados de libertad, un efecto de diseño o un
tamaño efectivo negativos— no se publica: se avisa y queda en el atributo
celdas_descartadas, con la métrica, la celda, el valor recibido y el motivo
(no_numerica o fuera_de_dominio). Una columna factor se lee por el texto
de sus niveles, no por sus códigos.
Data frame de clase medicion_calidad y medicion con el contrato
de medir(), más las columnas fuente y unidad: se acumula con
historico_calidad() y se une con rbind() como cualquier medición. Su
id_medida nombra la métrica y la celda estimada —objeto_medible— y no el
número de fila, así que dos tabulados de la misma entidad y la misma fecha
se pueden unir y evaluar juntos; la misma celda medida dos veces sí repite
su identificador, y evaluar() lo rechaza.
estadisticos_estimacion(), evaluar(), marco_cepal()
estimaciones <- data.frame( celda = c("Montevideo", "Interior"), stat = c(0.42, 0.38), cv = c(0.08, 0.34), n = c(1200L, 90L) ) medicion_desde_estimaciones( estimaciones, entidad = "ech2024", atributo = "celda", fuente = "survey 4.4, diseno complejo declarado por el equipo" )estimaciones <- data.frame( celda = c("Montevideo", "Interior"), stat = c(0.42, 0.38), cv = c(0.08, 0.34), n = c(1200L, 90L) ) medicion_desde_estimaciones( estimaciones, entidad = "ech2024", atributo = "celda", fuente = "survey 4.4, diseno complejo declarado por el equipo" )
Ejecuta todas las métricas instanciadas de un modelo_calidad. Cada fila es
una medida reutilizable y conserva el identificador y la fecha de la corrida.
medir( modelo, datos, id_medicion = NULL, fecha = Sys.time(), aplicabilidad = NULL, proteger_datos_personales = TRUE, columnas_personales = character(), validadores_personales = NULL )medir( modelo, datos, id_medicion = NULL, fecha = Sys.time(), aplicabilidad = NULL, proteger_datos_personales = TRUE, columnas_personales = character(), validadores_personales = NULL )
modelo |
Primer argumento. Objeto operativo creado por |
datos |
Segundo argumento. Data frame para una sola entidad o lista con nombre de data frames para varias entidades. Es la tabla que se mide, no un perfil ni un modelo. Con una lista, tienen que estar las tablas de todas las entidades que el modelo mide; si falta alguna se rechaza antes de medir, nombrando todas las que faltan y cuáles se recibieron. Una tabla que el modelo no pide se ignora: no hace falta que la lista coincida exactamente con las entidades, sólo que no falte ninguna. |
id_medicion |
Identificador de corrida. Si se omite, se genera uno. Cada
medida lleva un |
fecha |
Fecha y hora de la corrida. Se guarda en UTC; un texto
— |
aplicabilidad |
Lista con nombre por columna, donde cada elemento es
una fórmula que dice en qué filas esa columna corresponde —por ejemplo
Es la misma declaración que recibe Sin declaración toda la tabla aplica y el resultado es el de siempre. |
proteger_datos_personales |
Si se enmascaran los candidatos de
proximidad que corresponden a columnas personales. Por omisión |
columnas_personales |
Columnas que traen datos personales, declaradas
con la misma forma que acepta |
validadores_personales |
Pack o lista nombrada de funciones de
validación, con la misma forma que acepta |
Data frame S3 de clase medicion, con una fila por objeto medido.
Los booleanos se almacenan como 0 y 1 en la columna común resultado.
orientacion conserva si un valor alto expresa conformidad, si un valor
alto expresa defecto o si esa lectura no aplica. Algunas métricas
que trabajan con un vocabulario o un alcance parcial agregan un atributo
alcance_metricas con sus conteos y límites. Si una métrica no puede
medirse, no crea filas ni ceros: deja el motivo y cómo resolverlo en el
atributo cobertura_metricas, con un estado que distingue tres causas:
sin_valores cuando su universo no tiene valores, contrato_incompleto
cuando su contrato no trae un campo que necesita, y no_medible cuando su
método falló sobre estos datos —una columna sin dos valores para
ErrorEstandar, un atributo que la tabla no trae, una regla que devuelve
NA—, devolvió una salida que no cumple el contrato de metodo, o no
devolvió ninguna medida sobre un universo que sí tiene valores. Cuando el
método falló, el motivo conserva su mensaje, con los valores de las
columnas personales enmascarados si proteger_datos_personales = TRUE, y
medir() avisa: la falla ya no se lleva la medición de las demás
métricas, pero tampoco queda muda. Y cuando sí pudo medirse
pero sobre menos elementos de los que hay en su universo aplicable —una
métrica por celda no mide la celda vacía, que no produce medida ni cuenta
como incumplimiento—, el atributo alcance_medidas publica cuántos midió de
cuántos, con su unidad: sin ese número, el agregado de tres celdas de cuatro
no se distingue del de cuatro. Viaja al tablero, se imprime con la medición y
se publica en el informe. También conserva
configuracion_modelo y configuracion_aplicabilidad, descripciones de la
política usada para que una deriva posterior pueda distinguir modelo de
datos. Si el modelo declara un marco, la medición conserva tambien
marco_calidad; la configuracion del modelo registra su nombre, sus pares
dimension-factor y el tipo_resultado de cada metrica.
Esa descripcion es insensible al orden de la declaracion: declarar las
mismas metricas, o los mismos pares del marco, en otro orden no cambia el
modelo, porque un modelo es un conjunto de instrumentos y un marco un
conjunto de pares. El orden de los atributos de una instancia sí es
parte del modelo, porque el metodo recibe las columnas en ese orden. Y el
metodo de medicion entra en la descripcion solo si lo declaro quien
llama: el metodo por omision de una metrica es del paquete y su identidad ya
viaja en el nombre de la metrica, de modo que un cambio interno de lupa no
se lee como un cambio del modelo del usuario.
nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo, nombre_especifico = "NoNuloEdad") instancia <- instanciar(especifica, "personas", "edad") medir(modelo(instancia), data.frame(edad = c(20, NA, 35)))nucleo <- metricas_nucleo() especifica <- especializar(nucleo$NoNulo, nombre_especifico = "NoNuloEdad") instancia <- instanciar(especifica, "personas", "edad") medir(modelo(instancia), data.frame(edad = c(20, NA, 35)))
Devuelve las tres métricas base que pueden medirse con el contrato de
referencial(). CorrectitudSemFuerte verifica que la identificación exista;
CorrectitudSemDebil comprueba el par identificación–valor; RatioCobertura
mide qué proporción del universo completo de claves aparece en la entidad.
Los ratios de correctitud se obtienen mediante agregar() con "ratio".
Las tres métricas aceptan normalizar, proximidad, metodo, p, umbral,
max_pares y nucleos. normalizar = NULL hereda el perfil declarado por
referencial(). La normalización sólo cambia la representación usada para
emparejar: no modifica los datos. La proximidad es evidencia para los
valores ausentes y nunca cambia su veredicto; si el paquete opcional
stringdist no está
instalado, se declara que no se calculó. Se calcula una sola vez por valor
fallido distinto y la evidencia se reparte a las filas repetidas. El alcance
conserva por separado n_fallos (filas),
n_valores_fallidos_distintos, n_valores_fallidos_comparados y los pares
comparados; así el límite no depende del orden ni de la frecuencia de las
filas.
metricas_referencial()metricas_referencial()
Los números se emparejan por su valor. El mismo número guardado como entero
en una tabla y como doble en la otra coincide —100000L y 1e5—, y dos
dobles distintos no coinciden aunque se escriban igual con 15 cifras
—0.1 + 0.2 y 0.3—. Lo mismo vale para metricas_nucleo() cuando compara
filas por sus atributos, como en EntidadDuplicada. Los instantes y las
fechas se emparejan también por su valor, con su fracción: dos instantes del
mismo segundo no coinciden.
Un texto con bytes que no son UTF-8 válido —lo que deja un archivo latin1
leído sin declarar su codificación— se empareja por sus bytes, como %in%:
dos valores así distintos no coinciden entre sí. La proximidad no los
compara, porque una distancia de texto no se mide sobre bytes que no son
texto, y su motivo declara cuántos quedaron afuera.
Los valores ausentes no generan medidas de correctitud: corresponden a la
dimensión Completitud. La cobertura ignora claves ausentes en el objetivo y
no permite que duplicados inflen el resultado. El universo se cuenta con la
misma identidad con que se empareja: dos claves del padrón que la
normalización funde —A y a— son una sola clave del universo, y
n_referencial cuenta las claves de ese universo. Sobre un referencial sin
claves, RatioCobertura no se mide: la cobertura de un universo vacío no es
una proporción, y la métrica queda sin_valores con ese motivo, igual que
cuando la entidad no tiene filas o ningún valor en los atributos ligados:
lo que no se pudo medir no se publica como una cobertura cero.
Lista con tres objetos metrica_generica —CorrectitudSemFuerte,
CorrectitudSemDebil y RatioCobertura—, listos para instanciar contra un
referencial.
Son definiciones: no miden hasta que se las instancia.
referencial(), metricas_nucleo(), agregar()
ref <- referencial( data.frame(id = 1:3, nombre = c("Ana", "Bruno", "Carla")), "id", "nombre", completo = TRUE, alcance = "padr\u00f3n de ejemplo" ) m <- metricas_referencial() fuerte <- instanciar(especializar(m$CorrectitudSemFuerte), "personas", "id", referencial = ref) medir(modelo(fuerte), data.frame(id = c(1, 4)))ref <- referencial( data.frame(id = 1:3, nombre = c("Ana", "Bruno", "Carla")), "id", "nombre", completo = TRUE, alcance = "padr\u00f3n de ejemplo" ) m <- metricas_referencial() fuerte <- instanciar(especializar(m$CorrectitudSemFuerte), "personas", "id", referencial = ref) medir(modelo(fuerte), data.frame(id = c(1, 4)))
metrica() declara una métrica genérica y devuelve una fábrica de closures.
especializar() fija sus propiedades de configuración; la closure específica
resultante puede instanciarse sobre distintas columnas o tablas.
instanciar() liga la métrica a objetos concretos y materializa el método de
medición. modelo() reúne métricas instanciadas sin calcular un índice global.
metrica( nombre, semantica, granularidad, tipo_resultado, propiedades = character(), dimension = NA_character_, factor = NA_character_, metodo = NULL, validar_propiedades = NULL, orientacion = if (tipo_resultado %in% c("booleano", "real")) "conformidad" else "no_aplica" ) especializar(metrica, nombre_especifico = NULL, ...) instanciar( metrica_especifica, entidad, atributos = character(), nombre_instancia = NULL, metodo = NULL, referencial = NULL ) propiedades_metrica(x) modelo(..., marco = NULL) metricas_nucleo()metrica( nombre, semantica, granularidad, tipo_resultado, propiedades = character(), dimension = NA_character_, factor = NA_character_, metodo = NULL, validar_propiedades = NULL, orientacion = if (tipo_resultado %in% c("booleano", "real")) "conformidad" else "no_aplica" ) especializar(metrica, nombre_especifico = NULL, ...) instanciar( metrica_especifica, entidad, atributos = character(), nombre_instancia = NULL, metodo = NULL, referencial = NULL ) propiedades_metrica(x) modelo(..., marco = NULL) metricas_nucleo()
nombre |
Nombre estable y legible. |
semantica |
Descripción de lo que mide la métrica. |
granularidad |
Uno de los niveles de ontología devueltos por
|
tipo_resultado |
|
propiedades |
Nombres de las propiedades que fija |
dimension, factor
|
Metadatos taxonómicos; no se usan para calcular
puntuaciones. Cada uno es una cadena no vacía o |
metodo |
Método predeterminado opcional. Es una función de |
validar_propiedades |
Función opcional que recibe la lista con nombre
enviada a |
orientacion |
Sentido de lectura del resultado: |
metrica |
Objeto de clase |
nombre_especifico |
Nombre de la especialización. Si se omite, conserva el nombre genérico. |
... |
En |
metrica_especifica |
Objeto de clase |
entidad |
Nombres de las tablas ligadas, en el orden que espera el método. |
atributos |
Nombres de las columnas ligadas, en el mismo orden. Son
los nombres que tiene la tabla: una columna llamada |
nombre_instancia |
Nombre de la instancia. Si se omite, se deriva de la especialización y los objetos ligados. |
referencial |
Objeto opcional creado por |
x |
Métrica genérica, específica o instanciada. |
marco |
Objeto opcional creado por |
metricas_nucleo() devuelve veintidós métricas automatizables una vez
declaradas sus propiedades; escala() y vigencia() hacen explícitos los
insumos expertos que algunas necesitan. metricas_referencial() aporta por
separado las tres métricas que consumen un padrón tabular. Consulte
catalogo_agesic() para la correspondencia completa.
En ReglaIntegridadInterEntidad, entidad y atributos se ligan como
c(referencia, dependiente) y c(clave_primaria, clave_foranea).
Esta implementación calcula cobertura PK/FK como resultado real y sólo cubre
una parte de la genérica del marco, que declara granularidad
conjuntoEntidades, resultado booleano y admite además una expresión
condicional. catalogo_agesic() deja visible esa cobertura parcial.
Pese a su nombre, ErrorEstandar sigue literalmente la semántica de la
tabla 16.5 del marco y devuelve la desviación estándar muestral sin
normalizar; exige al menos dos valores numéricos válidos y ninguno infinito
—con uno, la métrica se declara no medible, como Escala—. Por eso declara
tipo_resultado = "numero_real" y no admite agregar().
Formato acepta exactamente una de las propiedades expresion_regular,
diccionario o validador. Esta última permite conectar validadores
externos sin incorporarlos como dependencias.
Tanto Formato como ValoresPosiblesPorExtension omiten los valores NA:
un ausente no genera una medida en esas métricas y, por lo tanto, tampoco
integra el denominador de sus agregaciones. La completitud se mide por
separado con NoNulo; así un mismo ausente no se penaliza en dos factores.
NoNulo acepta el vector opcional valores_nulos para aplicar de forma
deliberada el diccionario de nulos que contempla el marco; sin configurarlo,
sólo considera los NA reales. Con opcional = TRUE mide sobre las celdas
presentes: la ausencia no es defecto y sólo cuentan como faltantes los
valores_nulos. Es como proponer_modelo() propone la NoNulo de una
columna declarada en columnas_opcionales de perfilar(), para medir sobre
el mismo universo que el perfil: con un NA y tres -999 en cuarenta
celdas, 36/39 = 0,923 y no 36/40.
ValoresPosiblesPorComprension sigue la misma convención y acepta un
predicado o un rango definido por minimo, maximo e inclusivo.
Las métricas de duplicación marcan todas las apariciones que participan
en un grupo repetido, no sólo la segunda y siguientes. AtributoDuplicado
omite ausentes. ConjuntoAtributosDuplicado compara las columnas ligadas y
EntidadDuplicada compara la fila completa cuando se instancia sin
atributos. Si se ligan atributos de clave, sigue la semántica del marco:
marca filas con la misma clave cuyos demás valores son iguales o ausentes en
alguna de las dos. En las comparaciones exactas, los NA forman parte de la
combinación comparada.
EntidadContradictoria compara los valores distintos de un atributo con la
misma normalización y medida de similitud que
detectar_duplicados_aproximados(). Marca las filas cuyo valor tiene un
vecino por debajo de umbral, pero no modifica datos ni propone una limpieza.
Su alcance se conserva en el atributo alcance_metricas del resultado de
medir(): informa los valores distintos comparados, los pares evaluados y
los que quedaron bajo el umbral. max_valores permite acotar el vocabulario;
cuando se alcanza, el alcance declara el prefijo evaluado y los valores que
quedaron fuera.
DesactualizacionPorFormato devuelve TRUE cuando el valor no cumple el
formato vigente. Conforme a las tablas 16.29 y 16.30 del marco,
OportunidadAtributoPorFecha indica si la fecha es anterior o igual a
fecha_limite, y OportunidadAtributoPorIntervalo si pertenece al intervalo
cerrado [inicio_vigencia, fin_vigencia]; ambas son booleanas.
GradoOportunidadAtributoPorFecha y
GradoOportunidadAtributoPorIntervalo son extensiones propias basadas en el
curso CPAP, no entradas adicionales del catálogo AGESIC. Conservan la fórmula
continua max(0, min(1, 1 - (t1 - t2) / (t3 - t2))) para expresar cuánto
margen de utilidad queda. Exigen que t3 sea posterior a t2; un intervalo
de duración cero se rechaza porque no define el cociente. Todas estas
métricas omiten los valores NA: su ausencia corresponde a completitud y no
genera una segunda medida de incumplimiento.
Toda métrica que compara la columna con un valor temporal declarado avisa
cuando las dos puntas no son de la misma clase. Un Date se ancla a la
medianoche UTC y un POSIXct vale por su instante, así que mezclarlos cambia
el número sin que se vea: la fila que cae exactamente en un límite cambia de
veredicto con el huso de la sesión, y en las comparaciones por conjunto
—valores_nulos de NoNulo, diccionario de Formato, el dominio de
ValoresPosiblesPorExtension, el rango de ValoresPosiblesPorComprension y la
clave de un referencial()— ningún valor coincide con ninguno: la métrica
publicaría un incumplimiento que no existe, o —peor, en NoNulo— una
completitud perfecta sobre una columna llena de ausencias disfrazadas.
Declarar las dos puntas en la misma clase deja el número estable.
Desviación documentada del marco: en DensidadPonderada, un atributo
más crítico recibe un coeficiente mayor y, si falta, produce una penalización
mayor. El texto del marco indica acercar a cero el coeficiente de mayor
gravedad, lo que penalizaría menos el ausente crítico y contradice el sentido
de la ponderación. Los coeficientes deben estar en [0, 1] y sumar 1.
tipo_resultado es el contrato canónico que consultan las agregaciones. Las
unidades no forman parte de este núcleo porque el marco presenta ambas
nociones de forma inconsistente y sólo el tipo permite validar las fórmulas.
El argumento referencial se conserva sin transformación dentro de la
instancia; metricas_referencial() declara y valida el contrato específico
de correctitud semántica y cobertura.
metrica() y especializar() devuelven closures S3;
instanciar() devuelve una metrica_instanciada; modelo() devuelve un
modelo_calidad; metricas_nucleo() devuelve una lista de métricas
genéricas. propiedades_metrica() devuelve un data frame con las
propiedades declaradas y si ya fueron configuradas.
metodo
El método tiene la firma function(tablas, instancia). tablas es una lista
con nombre de data frames, incluso cuando medir() recibió una sola tabla.
instancia expone entidad, atributos, configuracion, referencial y
declaracion; el método decide cómo interpretar esos vínculos.
Debe devolver un data frame con exactamente una observación por objeto medido y, como mínimo, estas columnas:
resultado: valor medido. Debe respetar tipo_resultado: lógicos sin NA
para "booleano"; números finitos en [0, 1] para "real"; números
finitos para "numero_real"; enteros o duraciones no negativos para los
tipos homónimos;
entidad: nombre de la tabla a la que corresponde la medida;
atributo: nombre de la columna o NA_character_ cuando no corresponde;
fila: posición de la fila o NA_integer_ para resultados agregados;
objeto: etiqueta legible y estable del objeto medido.
medir() hace cumplir el contrato: una salida sin esas columnas, con un
resultado fuera de su tipo, con más de una observación para el mismo
objeto, con una entidad que no está ligada a la instancia, con un
atributo que no es una columna de las tablas que recibió el método —o
varias unidas con +—, con una fila que no es una posición entera o que
falta en una métrica por celda o por fila, o con una fila fuera del
universo que la métrica declara con su propiedad aplicable —una métrica con
aplicable mide sólo ese universo, y alcance_medidas cuenta sobre él—,
deja la métrica no_medible en
cobertura_metricas, igual que un método que aborta: medir() avisa y
las demás métricas se miden igual.
fila es la posición en la tabla que recibe el método. Con
aplicabilidad, medir() le pasa la tabla recortada al universo aplicable, y
traduce las fila de un método propio a la posición en la tabla original; la
etiqueta objeto queda como la escribió el método.
Las columnas adicionales se descartan. Un metodo pasado a instanciar()
reemplaza el predeterminado sólo para esa instancia. El ejemplo ejecutable
muestra la cadena genérica → específica → instanciada completa.
propiedades declara los nombres que puede recibir especializar(). Sin
validar_propiedades, todas son obligatorias. Con un validador propio, éste
recibe la lista configuracion, debe rechazar combinaciones inválidas y puede
devolver sólo el subconjunto activo o añadir valores predeterminados, pero no
propiedades ajenas a la declaración. propiedades_metrica() permite consultar
los nombres aceptados sin inspeccionar atributos internos de las closures.
AGESIC (2020). Marco de trabajo para la Gestión de la Calidad de Datos en Gobierno Digital, versión 1.6, Presidencia de la República, Uruguay.
Curso CPAP, material Evaluación de Calidad: fórmula continua de
oportunidad implementada bajo los nombres GradoOportunidadAtributo*.
referencial(), agregar(), medir(), proponer_modelo()
nucleo <- metricas_nucleo() no_nulo <- especializar( nucleo$NoNulo, nombre_especifico = "NoNuloEdad" ) instancia <- instanciar(no_nulo, entidad = "personas", atributos = "edad") modelo_calidad <- modelo(instancia) medir(modelo_calidad, data.frame(edad = c(20, NA, 35))) # Las fábricas también se pueden encadenar: genérica() -> específica(). instancia_directa <- nucleo$NoNulo()( entidad = "personas", atributos = "edad" ) propiedades_metrica(nucleo$Formato) # Métrica propia: las dos llamadas encadenadas son especializar e instanciar. metodo_origen <- function(tablas, instancia) { x <- tablas[[instancia$entidad]][[instancia$atributos]] filas <- seq_along(x) data.frame( resultado = !is.na(x) & nzchar(x), entidad = instancia$entidad, atributo = instancia$atributos, fila = filas, objeto = paste0(instancia$entidad, "$", instancia$atributos, "[", filas, "]") ) } OrigenDeclarado <- metrica( "OrigenDeclarado", "Indica si se declar\u00f3 el origen del registro.", "instanciaAtributo", "booleano", orientacion = "conformidad", dimension = "Trazabilidad", factor = "Origen documentado", metodo = metodo_origen ) origen <- OrigenDeclarado()( entidad = "entrega", atributos = "origen" ) medir( modelo(origen), data.frame(origen = c("sistema_a", "", NA), stringsAsFactors = FALSE) ) # Especialización oficial de teléfono fijo según el formato vigente del PNN. telefono_pnn <- especializar( nucleo$DesactualizacionPorFormato, nombre_especifico = "TelefonoFijoPNN", expresion_regular = "^[0-9]{8}$" ) # La métrica oficial es booleana y recibe la fecha límite Tf. a_tiempo <- especializar( nucleo$OportunidadAtributoPorFecha, nombre_especifico = "EntregaATiempo", fecha_limite = as.Date("2026-06-30") ) medir( modelo(instanciar(a_tiempo, "entregas", "fecha")), data.frame(fecha = as.Date(c("2026-06-29", "2026-07-01"))) ) # La extensión continua conserva cuánto margen de utilidad queda. grado <- especializar( nucleo$GradoOportunidadAtributoPorFecha, fecha_solicitud = as.Date("2026-06-01"), fecha_fin_utilidad = as.Date("2026-07-01") ) # Formato(NumeroDocumento, DNIC) se obtiene conectando el validador incluido: cedula_dnic <- especializar( nucleo$Formato, nombre_especifico = "NumeroDocumentoDNIC", validador = validar_ci_uy )nucleo <- metricas_nucleo() no_nulo <- especializar( nucleo$NoNulo, nombre_especifico = "NoNuloEdad" ) instancia <- instanciar(no_nulo, entidad = "personas", atributos = "edad") modelo_calidad <- modelo(instancia) medir(modelo_calidad, data.frame(edad = c(20, NA, 35))) # Las fábricas también se pueden encadenar: genérica() -> específica(). instancia_directa <- nucleo$NoNulo()( entidad = "personas", atributos = "edad" ) propiedades_metrica(nucleo$Formato) # Métrica propia: las dos llamadas encadenadas son especializar e instanciar. metodo_origen <- function(tablas, instancia) { x <- tablas[[instancia$entidad]][[instancia$atributos]] filas <- seq_along(x) data.frame( resultado = !is.na(x) & nzchar(x), entidad = instancia$entidad, atributo = instancia$atributos, fila = filas, objeto = paste0(instancia$entidad, "$", instancia$atributos, "[", filas, "]") ) } OrigenDeclarado <- metrica( "OrigenDeclarado", "Indica si se declar\u00f3 el origen del registro.", "instanciaAtributo", "booleano", orientacion = "conformidad", dimension = "Trazabilidad", factor = "Origen documentado", metodo = metodo_origen ) origen <- OrigenDeclarado()( entidad = "entrega", atributos = "origen" ) medir( modelo(origen), data.frame(origen = c("sistema_a", "", NA), stringsAsFactors = FALSE) ) # Especialización oficial de teléfono fijo según el formato vigente del PNN. telefono_pnn <- especializar( nucleo$DesactualizacionPorFormato, nombre_especifico = "TelefonoFijoPNN", expresion_regular = "^[0-9]{8}$" ) # La métrica oficial es booleana y recibe la fecha límite Tf. a_tiempo <- especializar( nucleo$OportunidadAtributoPorFecha, nombre_especifico = "EntregaATiempo", fecha_limite = as.Date("2026-06-30") ) medir( modelo(instanciar(a_tiempo, "entregas", "fecha")), data.frame(fecha = as.Date(c("2026-06-29", "2026-07-01"))) ) # La extensión continua conserva cuánto margen de utilidad queda. grado <- especializar( nucleo$GradoOportunidadAtributoPorFecha, fecha_solicitud = as.Date("2026-06-01"), fecha_fin_utilidad = as.Date("2026-07-01") ) # Formato(NumeroDocumento, DNIC) se obtiene conectando el validador incluido: cedula_dnic <- especializar( nucleo$Formato, nombre_especifico = "NumeroDocumentoDNIC", validador = validar_ci_uy )
Convierte las filas con incluir == TRUE en métricas instanciadas y las
reúne mediante modelo(). No vuelve a examinar los datos ni ejecuta una
medición.
modelo_desde_propuesta(propuesta)modelo_desde_propuesta(propuesta)
propuesta |
Objeto creado por |
Objeto modelo_calidad con las métricas que la propuesta declaró,
ya instanciadas y reunidas por modelo(), listo para medir(). No
vuelve a examinar los datos ni ejecuta ninguna medición.
proponer_modelo(), modelo(), medir()
datos <- data.frame(x = c(1, NA, 3)) propuesta <- proponer_modelo(perfilar(datos), datos) modelo_propuesto <- modelo_desde_propuesta(propuesta) medir(modelo_propuesto, datos)datos <- data.frame(x = c(1, NA, 3)) propuesta <- proponer_modelo(perfilar(datos), datos) modelo_propuesto <- modelo_desde_propuesta(propuesta) medir(modelo_propuesto, datos)
La normalizacion afecta unicamente la representacion usada para comparar;
nunca modifica los datos de entrada. La descomposicion canonica y el orden
de sus marcas son siempre activos para que NFC y NFD sean equivalentes en el
subconjunto latino cubierto por lupa: las letras latinas precompuestas, las
marcas combinantes de U+0300 a U+0332 ordenadas por su clase combinante
canónica, y los signos Kelvin y Ångström, que Unicode declara iguales a la
K y a la Å. Las tablas se generan desde Unicode
(data-raw/normalizacion_unicode.R). Los pasos optativos de ligaduras y ancho
completo tambien se aplican por punto de codigo, despues de declarar como
UTF-8 los bytes validos, por lo que no dependen de LC_CTYPE. Bajar a
minusculas tampoco depende del locale: usa un mapa explicito para el latin
acentuado, el griego, el cirilico, el armenio y el ancho completo, y lo que
queda fuera de ese mapa se conserva. Para comparar, la sigma final griega se
iguala a la sigma, como en el plegado de caja de Unicode: una palabra griega
en mayúsculas y la misma en minúsculas, escrita con su sigma final, son el
mismo valor.
normalizacion( minusculas = TRUE, espacios = TRUE, acentos = TRUE, comillas = TRUE, puntuacion = FALSE, ligaduras = FALSE, ancho = FALSE, proteger = c("ñ", "ü", intToUtf8(c(103L, 771L))) )normalizacion( minusculas = TRUE, espacios = TRUE, acentos = TRUE, comillas = TRUE, puntuacion = FALSE, ligaduras = FALSE, ancho = FALSE, proteger = c("ñ", "ü", intToUtf8(c(103L, 771L))) )
minusculas, espacios, acentos, comillas, puntuacion, ligaduras, ancho
|
Activan el paso correspondiente. |
proteger |
Grafemas cuyas marcas deben conservarse al quitar acentos.
Puede incluir una base seguida de una o más marcas combinantes, como
la secuencia |
El paso espacios lleva primero al espacio común la tabulación, el salto de
línea, el retorno de carro, el avance de página, la tabulación vertical y los
espacios Unicode, y borra los controles de transporte; al final colapsa y
recorta los blancos. Como va antes que comillas, una comilla junto a una
tabulación o un salto de línea se trata igual que junto a un espacio.
El paso comillas quita una comilla de apertura cuando abre una palabra —la
precede un blanco o el inicio y la sigue una letra— y una de cierre o un
apóstrofo cuando la cierra —la precede una letra y la sigue un blanco o el
final—; los apóstrofos tipográficos que quedan se escriben '. La comilla
simple ASCII cierra pero no abre, porque también es el apóstrofo de una
elisión: 'Piedras' se compara como 'piedras, mientras que la misma
palabra entre comillas simples tipográficas se compara como piedras.
Objeto de clase normalizacion_lupa: una lista con un elemento
logico por paso -minusculas, espacios, acentos, comillas,
puntuacion, ligaduras, ancho- y el vector proteger con los
grafemas cuyas marcas se conservan. Es una declaracion de como comparar:
no toca los datos, y la descomposicion canonica se aplica siempre.
# El perfil por omisión: minúsculas, espacios, acentos y comillas. normalizacion() # Comparar sin quitar acentos, para que "papa" y "papá" no se fusionen. La # eñe y la diéresis ya están protegidas en el perfil por omisión: "pena" y # "peña" no se fusionan nunca. perfil <- normalizacion(acentos = FALSE) perfil$acentos # La normalización sólo afecta la representación usada para COMPARAR; no # modifica los datos ni los conteos. Estas tres formas siguen siendo tres # valores distintos, y así se informan: lo que la normalización habilita es # que se reconozcan como variantes de la misma cosa al buscar duplicados. datos <- data.frame(ciudad = c("Montevideo", "MONTEVIDEO", "montevideo")) salida <- perfilar(datos, normalizar = normalizacion()) salida$columnas$n_distintos # 3, no 1# El perfil por omisión: minúsculas, espacios, acentos y comillas. normalizacion() # Comparar sin quitar acentos, para que "papa" y "papá" no se fusionen. La # eñe y la diéresis ya están protegidas en el perfil por omisión: "pena" y # "peña" no se fusionan nunca. perfil <- normalizacion(acentos = FALSE) perfil$acentos # La normalización sólo afecta la representación usada para COMPARAR; no # modifica los datos ni los conteos. Estas tres formas siguen siendo tres # valores distintos, y así se informan: lo que la normalización habilita es # que se reconozcan como variantes de la misma cosa al buscar duplicados. datos <- data.frame(ciudad = c("Montevideo", "MONTEVIDEO", "montevideo")) salida <- perfilar(datos, normalizar = normalizacion()) salida$columnas$n_distintos # 3, no 1
Las granularidades novena y décima del marco —una organización y un conjunto
de organizaciones— necesitan un dato que no está en los datos: qué bases
pertenecen a qué organismo. lupa no lo adivina, así que lo declara quien lo
sabe, con el mismo mecanismo que ya usa el conjunto de colecciones.
organizacion(nombre, colecciones)organizacion(nombre, colecciones)
nombre |
Nombre de la organización. Es su identidad dentro de un conjunto de organizaciones. |
colecciones |
Colecciones que le pertenecen: un vector de nombres, o una
lista de objetos de |
Es opcional. Un análisis de calidad no siempre tiene una organización
detrás, y nada obliga a pasar por estos niveles: existen para quien los
necesita. Sin declaración, agregar() a "organizacion" se niega y explica
cómo declararla, que es distinto de inventar una frontera que nadie nombró.
No pide una conexión. Una colección es una cosa viva —tablas de un motor—,
pero una organización es un enunciado sobre colecciones, y puede declarar
colecciones que viven en motores distintos. Para agregar, en cambio, las
medidas de sus colecciones tienen que venir de una misma corrida —un
id_medicion y una fecha—, porque un número describe un momento: las tablas
de motores distintos se miden juntas, y colecciones medidas en momentos
distintos se siguen cada una con historico_calidad(). agregar() lo dice
así cuando recibe partes de dos corridas.
Objeto S3 organizacion_lupa con nombre, declaradas,
n_declaradas y alias: el nombre del objeto de cada colección, con el
que agregar() la sigue reconociendo cuando la lista la renombró.
agregar(), coleccion(), granularidades()
organismo <- organizacion("Organismo A", c("padron", "tramites")) organismo organismo$declaradas # Cuando los nombres siguen una convencion, la agrupacion se deriva de ella # y despues se declara. `lupa` no aplica la regla por su cuenta -deducir el # dueno de una base a partir de su nombre es una inferencia, y la frontera # se declara-, pero tampoco obliga a escribir la lista a mano: la regla la # pone quien conoce la convencion, y con la forma que quiera. esquemas <- c("org_a_padron", "org_a_tramites", "org_b_registro", "cruce") sigla <- ifelse(grepl("^org_", esquemas), sub("_[^_]+$", "", esquemas), NA) partes <- split(esquemas[!is.na(sigla)], sigla[!is.na(sigla)]) organismos <- Map(organizacion, names(partes), partes) # Y lo que la convencion no alcanza -aca, una base de un integrador, que no # tiene dueno unico- se declara aparte, en vez de quedar afuera en silencio. organismos[["integrador"]] <- organizacion("integrador", esquemas[is.na(sigla)]) vapply(organismos, function(o) o$n_declaradas, integer(1))organismo <- organizacion("Organismo A", c("padron", "tramites")) organismo organismo$declaradas # Cuando los nombres siguen una convencion, la agrupacion se deriva de ella # y despues se declara. `lupa` no aplica la regla por su cuenta -deducir el # dueno de una base a partir de su nombre es una inferencia, y la frontera # se declara-, pero tampoco obliga a escribir la lista a mano: la regla la # pone quien conoce la convencion, y con la forma que quiera. esquemas <- c("org_a_padron", "org_a_tramites", "org_b_registro", "cruce") sigla <- ifelse(grepl("^org_", esquemas), sub("_[^_]+$", "", esquemas), NA) partes <- split(esquemas[!is.na(sigla)], sigla[!is.na(sigla)]) organismos <- Map(organizacion, names(partes), partes) # Y lo que la convencion no alcanza -aca, una base de un integrador, que no # tiene dueno unico- se declara aparte, en vez de quedar afuera en silencio. organismos[["integrador"]] <- organizacion("integrador", esquemas[is.na(sigla)]) vapply(organismos, function(o) o$n_declaradas, integer(1))
Un pack es una lista con nombres de funciones vectorizadas. No se registra
en estado global: puede definirse en otro paquete o en un script y conectarse
directamente con la propiedad validador de Formato o con
validadores_personales en perfilar(). Esta forma permite agregar países o
dominios sin modificar el núcleo de lupa; el pack uruguayo predeterminado
de perfilar() usa exactamente esta misma puerta.
pack_validadores(nombre, validadores, pais = NULL, descripcion = NULL) validadores_internacionales() validadores_uruguay()pack_validadores(nombre, validadores, pais = NULL, descripcion = NULL) validadores_internacionales() validadores_uruguay()
nombre |
Nombre del pack. |
validadores |
Lista con nombres de funciones que aceptan un vector y devuelven un vector lógico de igual longitud. |
pais |
Código ISO 3166 alpha-2 opcional del país al que pertenece el
pack. |
descripcion |
Descripción breve opcional. |
pack_validadores() devuelve un objeto S3 pack_validadores.
validadores_internacionales() y validadores_uruguay() devuelven packs
preparados para usar.
especializar(), validar_iso3166(), validar_ci_uy()
internacionales <- validadores_internacionales() internacionales$correo(c("[email protected]", "incorrecto")) # Un pack de otro país se construye sin registrar ni cambiar el núcleo. digito_rut_cl <- function(x) { uno <- function(valor) { z <- toupper(gsub("[.-]", "", valor)) if (!grepl("^[0-9]{7,8}[0-9K]$", z)) return(FALSE) cuerpo <- as.integer(strsplit(substr(z, 1, nchar(z) - 1), "")[[1]]) suma <- sum(rev(cuerpo) * rep(2:7, length.out = length(cuerpo))) esperado <- 11L - suma %% 11L # entero: su texto no sigue a `scipen` esperado <- if (esperado == 11) "0" else if (esperado == 10) "K" else as.character(esperado) identical(substr(z, nchar(z), nchar(z)), esperado) } ifelse(is.na(x), NA, vapply(as.character(x), uno, logical(1))) } chile <- pack_validadores( "Chile", list(rut = digito_rut_cl), pais = "CL", descripcion = "Validadores mantenidos por el proyecto consumidor." ) chile$rut(c("12.345.678-5", "12.345.678-4"))internacionales <- validadores_internacionales() internacionales$correo(c("[email protected]", "incorrecto")) # Un pack de otro país se construye sin registrar ni cambiar el núcleo. digito_rut_cl <- function(x) { uno <- function(valor) { z <- toupper(gsub("[.-]", "", valor)) if (!grepl("^[0-9]{7,8}[0-9K]$", z)) return(FALSE) cuerpo <- as.integer(strsplit(substr(z, 1, nchar(z) - 1), "")[[1]]) suma <- sum(rev(cuerpo) * rep(2:7, length.out = length(cuerpo))) esperado <- 11L - suma %% 11L # entero: su texto no sigue a `scipen` esperado <- if (esperado == 11) "0" else if (esperado == 10) "K" else as.character(esperado) identical(substr(z, nchar(z), nchar(z)), esperado) } ifelse(is.na(x), NA, vapply(as.character(x), uno, logical(1))) } chile <- pack_validadores( "Chile", list(rut = digito_rut_cl), pais = "CL", descripcion = "Validadores mantenidos por el proyecto consumidor." ) chile$rut(c("12.345.678-5", "12.345.678-4"))
Examina un data.frame, tibble o data.table y devuelve estadísticas
generales, métricas por columna, patrones, formatos de fecha y hallazgos
accionables. Todas las proporciones se expresan en [0, 1].
perfilar( datos, nombre = .nombre_de_los_datos(substitute(datos)), fecha = Sys.time(), muestra = 1e+05, max_patrones = 20, distinguir_mayusculas = TRUE, expandir = FALSE, umbral_alta_cardinalidad = 0.5, umbral_faltantes_sospechoso = 0.1, clave = NULL, umbral_faltantes_error = 0.4, umbral_patron_raro = 0.05, umbral_patron_dominante = 0.5, columnas_sin_ceros = character(), columnas_no_negativas = character(), columnas_opcionales = character(), columnas_codigo = character(), aplicabilidad = NULL, ausencia_estructural = TRUE, sentinelas_numericos = c(-9, -99, -999, -9999, 999), cadenas_ausencia = NULL, analizar_dependencias = TRUE, umbral_dependencia = 0.995, umbral_casi_clave_dependencia = 0.8, max_columnas_dependencias = 100L, datos_personales_permitidos = TRUE, proteger_datos_personales = TRUE, columnas_personales = character(), validadores_personales = NULL, umbral_documento_verificado = 0.9, muestra_validadores = 1000L, duplicados_aproximados = FALSE, normalizar = TRUE, max_filas_hallazgo = 1000L, umbral_orden_columnas = 0.95, max_columnas_orden = 20L, umbral_solapamiento_orden = 0.1, umbral_aritmetica = 0.9, min_filas_aritmetica = 3L, tolerancia_aritmetica = 1e-08, max_columnas_aritmetica = 20L, casi_duplicados_vocabulario = TRUE, max_proporcion_grupo_vocabulario = 0.5, umbral_variante_rara_vocabulario = 0.05, min_asimetria_vocabulario_corto = 10, min_asimetria_vocabulario = 2, min_participacion_dominante_vocabulario_corto = 0.5, variantes_equifrecuentes_vocabulario = FALSE, max_asimetria_equifrecuente_vocabulario = 2, max_comparaciones_dependencias = 200000L, max_trabajo_vocabulario = 2e+10, max_trabajo_dependencias = 1e+08, max_largo_valor_vocabulario = .MAX_LARGO_VALOR_CASI_DUPLICADOS, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, avisar_costo_tabla_ancha = TRUE, umbral_celdas_aviso_tabla_ancha = .UMBRAL_CELDAS_AVISO_TABLA_ANCHA )perfilar( datos, nombre = .nombre_de_los_datos(substitute(datos)), fecha = Sys.time(), muestra = 1e+05, max_patrones = 20, distinguir_mayusculas = TRUE, expandir = FALSE, umbral_alta_cardinalidad = 0.5, umbral_faltantes_sospechoso = 0.1, clave = NULL, umbral_faltantes_error = 0.4, umbral_patron_raro = 0.05, umbral_patron_dominante = 0.5, columnas_sin_ceros = character(), columnas_no_negativas = character(), columnas_opcionales = character(), columnas_codigo = character(), aplicabilidad = NULL, ausencia_estructural = TRUE, sentinelas_numericos = c(-9, -99, -999, -9999, 999), cadenas_ausencia = NULL, analizar_dependencias = TRUE, umbral_dependencia = 0.995, umbral_casi_clave_dependencia = 0.8, max_columnas_dependencias = 100L, datos_personales_permitidos = TRUE, proteger_datos_personales = TRUE, columnas_personales = character(), validadores_personales = NULL, umbral_documento_verificado = 0.9, muestra_validadores = 1000L, duplicados_aproximados = FALSE, normalizar = TRUE, max_filas_hallazgo = 1000L, umbral_orden_columnas = 0.95, max_columnas_orden = 20L, umbral_solapamiento_orden = 0.1, umbral_aritmetica = 0.9, min_filas_aritmetica = 3L, tolerancia_aritmetica = 1e-08, max_columnas_aritmetica = 20L, casi_duplicados_vocabulario = TRUE, max_proporcion_grupo_vocabulario = 0.5, umbral_variante_rara_vocabulario = 0.05, min_asimetria_vocabulario_corto = 10, min_asimetria_vocabulario = 2, min_participacion_dominante_vocabulario_corto = 0.5, variantes_equifrecuentes_vocabulario = FALSE, max_asimetria_equifrecuente_vocabulario = 2, max_comparaciones_dependencias = 200000L, max_trabajo_vocabulario = 2e+10, max_trabajo_dependencias = 1e+08, max_largo_valor_vocabulario = .MAX_LARGO_VALOR_CASI_DUPLICADOS, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, avisar_costo_tabla_ancha = TRUE, umbral_celdas_aviso_tabla_ancha = .UMBRAL_CELDAS_AVISO_TABLA_ANCHA )
datos |
Objeto que hereda de |
nombre |
Nombre descriptivo del objeto: una cadena no vacía. |
fecha |
Fecha y hora de la corrida. Se puede fijar para construir series reproducibles; se normaliza a UTC. |
muestra |
Máximo de filas usadas para patrones, inferencia de tipos,
formatos de fecha y dependencias funcionales. Use |
max_patrones |
Máximo de patrones mostrados por columna. |
distinguir_mayusculas |
Si se distinguen mayúsculas y minúsculas. |
expandir |
Si se emite un token por carácter en los patrones. |
umbral_alta_cardinalidad |
Umbral sobre la tasa de valores distintos de una columna categórica. No alcanza por sí solo: el hallazgo exige además al menos diez valores distintos, porque con pocos la tasa está dominada por el tamaño de la tabla —dos valores en tres filas dan 0,67 y superan cualquier umbral razonable— y una columna de dos valores no puede tener cardinalidad alta. |
umbral_faltantes_sospechoso |
Umbral inferior de faltantes. El hallazgo se activa al superarlo en sentido estricto. |
clave |
Nombres de las columnas que identifican una fila. Cuando se
declaran, la trazabilidad de cada hallazgo trae además el valor de esas
columnas para las filas señaladas, de modo que el caso se pueda verificar
en el sistema de origen sin abrir la tabla. El índice de fila se conserva
siempre. La clave declarada se trata como sensible: si la protección de
datos personales está activa y alguna de esas columnas se clasifica como
personal, sus valores salen enmascarados igual que la evidencia.
La comprobación de una clave tiene dos ejes independientes:
|
umbral_faltantes_error |
Umbral por encima del cual los faltantes son un error; la igualdad conserva la severidad sospechosa. |
umbral_patron_raro |
Máxima frecuencia de un patrón raro. La frontera es inclusiva: un patrón no dominante con exactamente esa proporción es raro, y sólo los que la superan quedan excluidos. |
umbral_patron_dominante |
Frecuencia mínima del patrón dominante. |
columnas_sin_ceros |
Nombres de columnas donde cero no es admisible. |
columnas_no_negativas |
Nombres de columnas que deben ser no negativas. |
columnas_opcionales |
Nombres de columnas donde la ausencia no es un
defecto. Su universo de completitud son las celdas presentes, y
|
columnas_codigo |
Nombres de columnas que son códigos, no
magnitudes: un código de catálogo, un departamento numerado, una clave
foránea. El paquete no los distingue por su forma de una magnitud entera
que llenó su rango —una edad de 18 a 90 repite valores igual que un código
de 1 a 284—, así que sin la declaración los mide como magnitudes. Una columna
declarada se perfila por sus valores y no recibe lo que sólo tiene sentido
sobre una magnitud: ni límites de Tukey ( |
aplicabilidad |
Lista con nombre por columna, donde cada elemento es
una fórmula de un solo lado evaluada sobre
Hasta dónde llega el universo. Gobierna todo el perfilado: los conteos,
las proporciones, los hallazgos y las acciones que propone
El universo tambien llega a la limpieza. El perfil guarda la regla, el plan
la lleva y La medición contra un marco recibe la misma declaración: |
ausencia_estructural |
Si se busca evidencia de que la ausencia de una
columna es por diseño. Con el mismo argumento viaja |
sentinelas_numericos |
Vector completo de valores numéricos que se
interpretan como ausencia. Se normaliza como un conjunto numérico, por lo
que el orden, el tipo entero o doble y los duplicados no cambian el
resultado. La lista por omisión es una sospecha del paquete y se aplica con una
reserva: un valor de esa lista que aparece una sola vez y cae
dentro del rango de los demás valores de la columna no se señala,
porque una aparición única es un número y no una convención de ausencia.
Un La reserva sólo rige para la lista por omisión. Si usted declara sus propios valores —o pide sentinelas_naniar, que es optar por una heurística más ancha—, se cuentan todos, incluida la aparición única. Ésa es también la forma de recuperar el caso que la reserva calla: declarar el valor lo vuelve a contar y además lo saca de los estadísticos del resumen, de la ley de Benford y de las relaciones aritméticas. |
cadenas_ausencia |
Cadenas que el usuario declara como ausencia,
además de la lista incorporada. Lo declarado atraviesa la guarda del
vocabulario: |
analizar_dependencias |
Si se buscan dependencias funcionales entre pares de columnas. Se aplica una sola muestra común a toda la tabla. |
umbral_dependencia |
Cumplimiento mínimo para informar una dependencia. |
umbral_casi_clave_dependencia |
Tasa de valores distintos a partir de la cual un determinante se descarta como casi-clave antes de agrupar. |
max_columnas_dependencias |
Máximo de columnas que intervienen en la búsqueda, cuyo costo crece cuadráticamente. |
datos_personales_permitidos |
Si la entrega admite datos personales.
El valor predeterminado no juzga su presencia: la clasificación se informa
con severidad |
proteger_datos_personales |
Si se reemplazan modas, ejemplos, evidencia
y estadísticos de orden concretos cuando |
columnas_personales |
Columnas que traen datos personales, declaradas
por quien conoce el dato. Existe porque el léxico de nombres de columna no puede ser completo:
una columna con documentos se puede llamar de cualquier manera, y ninguna
lista de nombres frecuentes la va a reconocer. Es el mismo patrón que
|
validadores_personales |
Pack o lista nombrada de funciones que reciben
un vector de texto y devuelven un lógico de igual longitud. |
umbral_documento_verificado |
Proporción mínima de valores que debe
aceptar un validador para clasificar una forma de documento como
|
muestra_validadores |
Máximo de valores usados en el filtro preliminar
de cada validador. Si la proporción preliminar ya queda bajo el umbral no
se valida la columna completa; use |
duplicados_aproximados |
|
normalizar |
Perfil de comparación que se conserva en |
max_filas_hallazgo |
Tope de índices de fila que conserva cada
trazabilidad disponible. Por defecto es |
umbral_orden_columnas |
Cumplimiento mínimo de una relación de orden
entre columnas comparables. Se usa |
max_columnas_orden |
Máximo de columnas numéricas o temporales que se
comparan entre sí para detectar relaciones de orden. Las columnas que
exceden el límite se conservan en |
umbral_solapamiento_orden |
Solapamiento mínimo de los rangos
intercuartiles para considerar que dos columnas representan magnitudes
comparables. Por defecto es |
umbral_aritmetica |
Proporción mínima de filas comparables que deben
satisfacer una identidad dentro de |
min_filas_aritmetica |
Mínimo de filas comparables necesario para
evaluar una candidata aritmética. Por omisión es |
tolerancia_aritmetica |
Tolerancia numérica relativa escalada usada al
comparar un valor observado y uno esperado. Por omisión es |
max_columnas_aritmetica |
Máximo de columnas numéricas que intervienen
en la búsqueda aritmética, cuyo costo crece cúbicamente. Por omisión es
|
casi_duplicados_vocabulario |
Lógico que activa el diagnóstico de
variantes casi duplicadas dentro del vocabulario de cada columna de texto.
Por defecto es |
max_proporcion_grupo_vocabulario |
Proporción máxima del vocabulario
que puede abarcar el grupo mayor para entregar grupos de variantes. Por
defecto es |
umbral_variante_rara_vocabulario |
Proporcion maxima de la columna que puede ocupar una variante breve para abrir la comparacion por una edicion. |
min_asimetria_vocabulario_corto |
Razon minima entre la frecuencia de una forma dominante y una variante breve para abrir la comparacion por una edicion. |
min_asimetria_vocabulario |
Razón mínima entre la frecuencia de la
forma dominante y la de la variante para abrir un grupo por la vía general
de distancia. Por omisión |
min_participacion_dominante_vocabulario_corto |
Proporcion minima de la columna que debe ocupar la forma dominante en la comparacion por una edicion. |
variantes_equifrecuentes_vocabulario |
Si se informa el diagnóstico
Está apagado por omisión y la razón está medida: sobre la batería de
31 tablas limpias produce un grupo sospechoso donde no hay defecto. Lo que
lo dispara no es el tamaño de la tabla sino el reparto: dos formas
parecidas con frecuencias del mismo orden lo activan igual con cuatro filas
que con quinientas, y en una tabla chica ese reparto sale por casualidad
más seguido. Decía «tablas de menos de veinte filas», y se midió de cuatro
a quinientas: dispara en todas. Es aditivo: encenderlo no cambia ni pierde ninguna
detección de |
max_asimetria_equifrecuente_vocabulario |
Razón máxima entre la
frecuencia mayor y la menor para considerar que dos formas se reparten la
columna. Con |
max_comparaciones_dependencias |
Tope de pares determinante-dependiente
que se comparan. El costo de las dependencias es del orden de
|
max_trabajo_vocabulario |
Tope de comparaciones de carácter para las
comparaciones de distancia del vocabulario. Comparar dos valores cuesta del
orden del producto de sus largos, así que el trabajo es la suma de ese
producto sobre todos los pares. Contar pares por longitud media subestima
las cadenas largas: medido, el mismo presupuesto compraba 5,3 millones de
unidades por segundo con valores de 900 caracteres y 44 millones con
valores de 40. Se combina con el límite interno de pares del detector y
manda el más restrictivo; ese límite interno no es un argumento de
|
max_trabajo_dependencias |
Tope predeterminado de unidades fila-par para
las dependencias. Se combina con |
max_largo_valor_vocabulario |
Maximo de caracteres permitido en cada
valor que entra en la comparacion de casi duplicados del vocabulario. Por
defecto es |
max_celdas_muestra |
Maximo de celdas que puede contener la muestra
comun de los diagnosticos que muestrean filas. Por defecto es |
max_bytes_muestra |
Maximo de bytes de la muestra materializada que
alimenta los diagnosticos que muestrean filas. Por defecto es |
avisar_costo_tabla_ancha |
Si es |
umbral_celdas_aviso_tabla_ancha |
Cantidad de celdas a partir de la cual
se emite el aviso de tabla ancha. Por defecto, |
Los umbrales de faltantes se aplican a la suma de ausentes reales y
faltantes disfrazados y son estrictos: la proporción debe superar el umbral
para generar el nivel correspondiente. La lista de cadenas está congelada con referencia a
naniar::common_na_strings 1.1.0 y suma extensiones habituales en datos
administrativos uruguayos. Las entradas que naniar expresa como patrones
escapados se adaptan a los signos literales de interrogación, asterisco y
punto porque aquí se comparan por igualdad. La lista no depende de la
versión instalada.
Los sentinelas numéricos predeterminados son -9, -99, -999, -9999 y
999. La lista es deliberadamente más corta que
naniar::common_na_numbers 1.1.0:
66, 77, 88 y 9999 también pueden
ser edades, códigos o años legítimos. sentinelas_numericos representa la
política completa, no una lista que se agrega silenciosamente: use
numeric() para desactivar todos los sentinelas numéricos, o
sentinelas_naniar para solicitar explícitamente la lista de naniar. La
política se normaliza como un conjunto numérico: el orden, el tipo entero o
doble, los duplicados y los NA no cambian su interpretación.
muestra limita el descubrimiento de patrones, la inferencia de tipos, la
detección de formatos de fecha y la búsqueda de dependencias funcionales.
Las demás métricas y hallazgos se calculan sobre todas las filas. En los
resúmenes que convierten texto con el tipo o formato descubierto en esa
muestra, los valores presentes que no se pueden convertir no se ocultan:
n_fechas_excluidas_granularidad los cuenta para fechas y
n_valores_excluidos_resumen para números, y el estado deja de ser un
cálculo completo. n_valores_excluidos_resumen cuenta también los valores no
finitos que el resumen deja afuera, y lo hace en cualquier clase: una columna
de fechas con un Inf publica n_valores_excluidos_resumen = 1,
n_fechas_resumidas sin contarlo y el estado
"calculados_sobre_valores". Y para las longitudes de texto,
n_longitudes_resumidas declara sobre cuántos valores se calcularon
longitud_minima, longitud_maxima y longitud_media: nchar() no puede medir
un valor cuyos bytes no son UTF-8 válido, y ése queda fuera del promedio. La causa
viaja aparte, en n_codificacion_invalida. Por eso
meta$filas_analizadas describe el máximo usado por los análisis muestreados,
no el alcance del perfil completo.
En cada fila de columnas, n_filas_analizadas_tipo y
muestreado_tipo_inferido declaran el alcance concreto de
proporcion_tipo_inferido; no debe interpretarse esa proporción como si
hubiera usado necesariamente toda la columna.
El tipo se infiere de los valores sólo en las columnas de texto: en las
demás es el tipo con que R las guarda —lógico, entero, doble, fecha—, que no
depende de los valores. Por eso una columna no textual sin ningún valor
conserva su tipo de almacenamiento, con proporcion_tipo_inferido en NA
porque ningún valor lo midió, y una columna de texto sin valores queda
"desconocido", porque ahí no hay nada de qué inferirlo.
Cuando el tipo inferido es "fecha" o "fecha-hora",
estado_tipo_inferido declara además cómo quedó establecida esa lectura:
"confirmado" si todo formato con casamientos es inequívoco, y
"candidato" si el veredicto se apoya en casamientos ambiguos — el caso de
una columna donde proporcion_tipo_inferido llega a 1 sin un solo valor
inequívoco. En los demás tipos la columna queda NA. El resumen impreso
anota (candidato) junto al tipo cuando corresponde; la conversión, el
rango temporal y la remediación ya exigían formatos confirmados y no
cambian.
En una columna temporal, minimo, maximo, media y mediana quedan en
NA y su valor viaja en minimo_fecha, maximo_fecha, media_fecha y
mediana_fecha, que son texto legible. desvio es la excepción y conviene
saberlo: no es un momento sino una duración, así que se informa como número,
y ese número está en segundos tanto para "fecha" como para
"fecha-hora", porque las dos clases se unifican en esa unidad antes de
resumirlas. Un desvío de 136610.4 sobre una columna de fechas son 1,6 días.
Ese detalle importa al comparar las dos puertas. perfilar_dbi() no clasifica
tipos: informa el que declara el motor, y un motor que no preserva DATE ni
BOOLEAN —SQLite guarda ambos como número— hace que esas columnas se midan
como números. La misma columna de fechas da entonces desvio en días por la
puerta DBI y en segundos por ésta, y su moda sale como el entero crudo del
motor en vez de la fecha formateada. No es una discrepancia de cálculo: cada
puerta describe lo que tiene delante, y lo que tiene delante es distinto.
Una columna cuyo año se expresa con dos dígitos se informa con su
tipo_inferido —"fecha" o "fecha-hora"— pero deja minimo_fecha,
maximo_fecha, media_fecha y mediana_fecha en NA. No es una omisión:
23 puede ser 1923 o 2023, y elegir el siglo para calcular un rango sería
inventarlo. El hallazgo anio_de_dos_digitos señala esas columnas, y el
rango aparece una vez que el usuario resuelve la ambigüedad.
Lo mismo vale para un valor que dos formatos confirmados de la misma columna
leen distinto: en una columna que tiene 13/06/2020 y 06/30/2020, el valor
01/12/2020 puede ser el 1 de diciembre o el 12 de enero, y no se decide por
la mayoría. Queda fuera del rango, y planificar_limpieza() bloquea la
conversión de la columna mientras queden valores así. El resumen de fechas
usa la misma regla de validez que la detección: un 99991231 que el formato
%Y%m%d no admite tampoco llega a maximo_fecha.
Cuando no sobrevive ningún valor utilizable, el estado distingue dos
afirmaciones que no son la misma: "sin_valores" dice que la columna no tenía
ningún valor presente en el universo que el resumen mide —vacía, todo NA,
o con la aplicabilidad declarada dejando ese universo sin valores— y
"sin_valores_utilizables" dice que sí los tenía y ninguno servía para el
resumen. La relación que se cumple con la fila es
n_aplicables - n_faltantes, no n - n_faltantes: con aplicabilidad declarada
los valores de afuera del universo se declaran en
n_presentes_fuera_de_aplicabilidad y no cuentan para este estado. Una columna de treinta Inf es el
segundo caso y no el primero: publica n_faltantes = 0 y n_distintos = 1,
así que decir «sin valores» de ella contradiría a su propia fila. La otra puerta
del segundo caso son los centinelas que se llevan todos los valores. La
presencia se define igual que en n_faltantes, que cuenta el NaN como
ausente: una columna de treinta NaN es "sin_valores".
La media se acumula por bloques y puede diferir de mean() en los últimos
bits; la mediana es exacta. El paquete no promete exactitud bit a bit para
la media —sí para la mediana, que se calcula con median() sobre el vector
entero y por eso no se reemplazó por el cuantil 0,5—. Medido: sobre veinte mil
valores de 1e12 y veinte mil de 1e-6 la diferencia es de 6,1e-05, y sobre
treinta mil valores con signo del orden de 1e9 es de 2,3e-10; con valores de
magnitud parecida no hay diferencia en ningún bit. Quien rehaga mean() sobre
una columna grande y vea otro último dígito está viendo esto y no un error.
Objeto S3 de clase perfil. Cada fila de hallazgos incluye
n_evaluados, n_afectados y unidad_conteo: son conteos de las unidades
declaradas (por ejemplo fila, columna, formato o par). En
mayusculas_inconsistentes, normalizacion_unicode y
casi_duplicados_vocabulario, la unidad es
valor_distinto: n_evaluados cuenta los valores distintos evaluados y
n_afectados los valores distintos que participan en la colisión. Su
trazabilidad sigue siendo por fila y enumera todas las filas que
contienen esos valores, no sólo las filas defectuosas; por eso su total
puede ser mayor que n_afectados. En casi_duplicados_vocabulario, la
traza incluye todas las filas cuyos
valores pertenecen al grupo elegido, incluida la forma dominante. La
distancia es una senal heuristica, no una prueba de que cada fila deba
corregirse; la evidencia declara cuantas filas mostradas pertenecen a las
formas variantes y cuantas a las formas dominantes.
En filas_duplicadas, el conteo y la traza incluyen todas las filas
participantes de los grupos; el numero de excedentes queda en la
evidencia. Cuando el camino
no puede conocer un conteo, informa NA, nunca cero. La columna de lista
trazabilidad distingue disponible, truncada, no_aplica y
no_disponible; cuando corresponde conserva índices de fila acotados por
max_filas_hallazgo, el total conocido y el alcance. En
clave_con_ausentes, cuenta las filas con al menos un componente ausente
y su traza enumera esas filas. En clave_no_unica, cuenta las filas que
participan en colisiones entre claves completas y su traza excluye las
filas incompletas; ambas unidades son fila. Así, una colisión entre
ausentes no aparece como una repetición que refute la unicidad.
casi_duplicados_vocabulario, donde la traza mezcla filas de formas
variantes con filas de la forma dominante, conserva además
n_filas_formas_variantes y n_filas_formas_dominantes con el reparto
completo, y mostrados_formas_variantes y mostrados_formas_dominantes
con el reparto de lo que sobrevivió al truncado. En los tres diagnósticos
de unidad valor_distinto las filas de las formas variantes se entregan
primero —en mayusculas_inconsistentes y normalizacion_unicode, la forma
dominante de cada grupo es la guardada más veces—, de modo que el truncado
no se lleve lo accionable. Para
patron_raro,
el alcance puede ser completo, muestra_patrones,
patrones_parciales o muestra_patrones+patrones_parciales. El resumen y
el texto de evidencia de patron_raro muestran como maximo seis patrones,
pero la trazabilidad conserva los nombres de todos los patrones raros hasta
un limite de 5.000; patrones_parciales indica que se alcanzo ese limite,
no que se haya alcanzado el tope de presentacion.
Cuando se emite un hallazgo patron_raro, su evidencia incluye la
proporcion del patron dominante y cuantas filas pertenecen a patrones no
dominantes que superan umbral_patron_raro y por eso quedan excluidos.
Su n_evaluados es el universo de esas proporciones: las filas analizadas
que tienen valor —un ausente no tiene patrón—.
Si el patron dominante no alcanza umbral_patron_dominante, no se emite
el hallazgo: cobertura_diagnosticos declara la no medicion, su proporcion
observada y el argumento que se puede ajustar.
Si el conteo y la traza no coinciden, conserva el hallazgo y emite una
advertencia de clase lupa_trazabilidad_incoherente. La guarda compara el
total previo al truncado y respeta la unidad declarada.
Una columna compuesta no analizada conserva en la traza todas sus filas. Si una
columna de listas se reconoce como constante pero no se puede contar su
frecuencia, el conteo afectado queda en NA y cobertura_diagnosticos
explica la no evaluación.
Los índices no contienen valores. Usarlos para extraer filas de los datos
originales puede volver a exponer datos personales; el paquete no realiza
esa extracción y la protección de salidas no sustituye el control de acceso
a los datos de entrada.
En la evidencia de casi_duplicados_vocabulario, clase_diferencia puede
ser normalizacion_exacta (la coincidencia aparece después de normalizar),
dentro_de_palabra (la diferencia está dentro de un token),
token_completo (cambian tokens completos), token_unico (ambos valores
son un solo token y esa distinción estructural no aplica), mixta (el grupo
reúne aristas de más de una clase) o indeterminada (no hubo aristas
clasificables). Son categorías de evidencia, no veredictos de identidad.
cobertura_diagnosticos es una tabla hermana de hallazgos, con una fila
por diagnóstico que no pudo evaluarse o cuya enumeración quedó parcial y
las columnas diagnostico,
columna, motivo, como_resolverlo y dependencia. Incluye la falta de
stringdist, stringi, bit64 o sf, incluida la medición de secuencias
integer64 cuando falta bit64, y las zonas horarias POSIXt sin declarar.
Los patrones de frecuencia intermedia no se consideran desvios
del patron dominante: patron_raro es completo respecto de su criterio de
rareza cuando no hay recorte de trazabilidad. Si el conjunto de nombres
raros supera 5.000, cobertura_diagnosticos declara el recorte y su limite.
Si muestra deja filas fuera del descubrimiento de patrones y la muestra
no trae ningún patrón raro, cobertura_diagnosticos también lo declara: la
ausencia del hallazgo vale sólo para la muestra.
Quien decida automáticamente sobre un perfil debe revisar
nrow(perfil$cobertura_diagnosticos) además de las severidades: un perfil
sin hallazgos y con diagnósticos no evaluados no es un perfil limpio.
Cuando una clave declarada no queda plenamente verificada, meta$clave
conserva los estados de unicidad y ausencia de nulos, sus conteos y la
semántica usada por la trazabilidad. Los tres responden preguntas distintas
y no comparten universo:
unicidad se evalúa sólo entre las filas con la clave completa
(semantica = "claves_completas"), porque una repetición entre filas
incompletas no viola la unicidad: en SQL dos NULL no son iguales.
filas_evaluadas cuenta esas filas y filas_totales la tabla entera. Su
estado es "verificada", "refutada", "no_verificada" cuando no se
pudo comparar, o "sin_casos_evaluables" cuando ninguna fila tiene la
clave completa: ahí la unicidad sería cierta sobre un conjunto vacío, que
es cierto y engañoso a la vez, y por eso tiene estado propio.
ausencia_nulos responde si todos los componentes están presentes.
Su hallazgo asociado, clave_con_ausentes, cuenta las filas con al menos
un componente ausente y conserva sus índices.
clave_no_unica sólo se emite cuando hay valores repetidos entre las
filas completas y usa filas_evaluadas como n_evaluados; no convierte
una colisión entre ausentes en una violación de unicidad.
trazabilidad conserva la semántica de R, que es la que localiza las
filas, e informa en colisiona_con_ausentes si el localizador queda
ambiguo porque dos filas con ausentes comparten representación.
El campo unidad dice en qué unidad están las cifras que la fila publica
—minimo, maximo, media, mediana, desvio—, y queda en NA cuando la
columna no declara ninguna. Es una sola pregunta, y por eso no se confunde con
numero_texto_unidad, que es la unidad que un valor de texto traía pegada al
número ("12 kg"), ni con la unidad de clasificar_variables(), que es la
unidad declarada del dato.
Sin este campo, cuatro columnas medidas en unidades distintas publicaban filas
idénticas: units::set_units(c(1, 2, 3), "m") y la misma en "km" dan las dos
media = 2 y desvio = 1, y un Period de dos días publicaba 172800 sin
decir que eran segundos. Por clase:
units y difftime: la unidad declarada, en forma canónica (km/h, m^2,
kg*m/s^2). difftime publica sus estadísticos en esa unidad: una
columna de minutos publica media = 75 con unidad = "mins". Se abstuvo
mientras no existía dónde publicar la unidad —un 75 sin decir de qué no es
una medida—, y era el único que se abstenia: Date y POSIXt publican
desvio en segundos desde antes. Los valores van en la unidad declarada por
la columna, sin convertir a segundos: convertirlos publicaría un número que
no está en la columna.
Period, Duration, Date y POSIXt: "segundos", que es la unidad en la
que sale desvio y que antes sólo estaba dicha en prosa.
cualquier otra columna que declare un atributo units —eso circula como
metadato en datos importados— publica lo declarado, porque sus cifras están
en esa unidad.
el resto: NA. El campo no inventa una unidad donde no hay ninguna.
Y se calla cuando la fila no publica ninguna cifra cuantitativa, o sea
cuando estado_resumen_cuantitativo vale "no_aplica": el atributo units lo
puede llevar cualquier clase, y un factor con attr(x, "units") <- "kg"
publicaba unidad = "kg" al lado de media = NA. Una unidad de cifras que no
existen no contesta la pregunta del campo. El discriminador es ese estado y no
una lista de clases, así que una columna units de puros NA sí declara
su unidad: su clase se resume, y la unidad dice en qué estarían sus cifras.
Si una columna declara más de una unidad —un atributo units de largo
mayor que uno, que ningún objeto units produce pero un dato importado sí—,
el campo las publica todas separadas por coma. Ese valor no es una unidad: dice
que la declaración está mal. Quedarse con la primera sería elegir una por el
usuario, que es el defecto que este campo vino a cerrar.
Y comparar_perfiles() compara este campo como un aspecto propio, con la
severidad de un cambio de tipo: publicar la unidad sin compararla dejaba en pie
el defecto entero, porque el problema era que una entrega donde cambió la
unidad no mostraba cambio.
Una columna de períodos expresados sólo como mes y año informa la
granularidad "mes" en formatos_fecha y deja los resúmenes de fecha en
NA con estado_resumen_cuantitativo = "granularidad_incompleta": asignar
el día 1 para obtener un mínimo o una media también sería inventar un dato.
Si esos períodos son minoritarios dentro de una columna que también contiene
fechas con día, los resúmenes se calculan sólo sobre las fechas completas y
declaran estado_resumen_cuantitativo = "calculados_sobre_dias", junto con
n_fechas_resumidas y n_fechas_excluidas_granularidad. El mínimo y el máximo
son entonces condicionales al subconjunto con día; no representan un rango
de toda la columna.
Los dos conteos que acompañan a ese estado cuentan cosas distintas.
n_fechas_excluidas_granularidad cuenta sólo las fechas que quedaron
fuera por ser de granularidad mes, y queda en NA cuando los formatos se
descubrieron sobre una muestra: ese número se deriva de los formatos
detectados en la muestra mientras el resumen recorre la columna entera, así
que atribuir la causa exigiría rehacer la detección sobre todo, que es el
costo que el muestreo existe para evitar. n_valores_excluidos_resumen es el
campo general —el mismo que usan las columnas numéricas— y cuenta todo
valor presente que no sostiene el resumen, sea un período de mes o un texto
que ningún formato pudo leer. Siempre que ese total sea mayor que cero, el
perfil agrega una fila resumen_cuantitativo en cobertura_diagnosticos,
haya muestreo o no.
Para números ordinarios, los estadísticos cuantitativos se calculan sólo con
valores finitos; n_nan, n_infinito_positivo y n_infinito_negativo
declaran lo excluido. En columnas integer64 que exceden el entero máximo
representable exactamente por double, minimo y maximo quedan en NA y
los extremos exactos se conservan en minimo_exacto y maximo_exacto.
Cuando la conversión de texto deja valores presentes afuera —haya muestreo o
no, porque la conversión descarta lo que no puede leer en los dos casos—,
n_valores_excluidos_resumen y
estado_resumen_cuantitativo = "calculados_sobre_valores" declaran ese
alcance parcial.
Los limites de Tukey se publican solo cuando hay al menos 20 valores finitos
y el recorrido intercuartilico es positivo. Si falta alguna condicion,
n_outliers queda en NA y cobertura_diagnosticos explica el motivo y
como reunir evidencia suficiente; un IQR cero no convierte una categoria
minoritaria en outlier.
El paquete distingue lo que el usuario declara de lo que él mismo
sospecha, y esa distinción gobierna los resúmenes. Cuando se reemplaza
sentinelas_numericos, esos valores son una declaración de ausencia y salen
de media, mediana, minimo, maximo, desvio y n_outliers, igual que
salen los NA y las filas que aplicabilidad deja fuera del universo;
n_valores_excluidos_resumen y estado_resumen_cuantitativo declaran ese
alcance. Salen también de la ley de Benford y de las relaciones aritméticas,
que leen la columna como magnitud: la misma celda no puede ser ausencia en
el resumen y valor en un diagnóstico. La lista por omisión —-9, -99,
-999, -9999 y 999— es en cambio una conjetura del paquete: se
informa como faltantes_disfrazados
pero no se retira de los resúmenes, porque 999 también puede ser un
dato legítimo y actuar sobre una sospecha cambiaría números que nadie pidió
cambiar.
moda, frecuencia_moda, n_distintos, tasa_distintos y longitud_*
describen la representación almacenada y siguen contando esos valores,
del mismo modo que cuentan un Inf que los estadísticos excluyen o un
"N/A" textual. Por eso moda puede quedar fuera del rango que informan
minimo y maximo: no es una contradicción sino la diferencia entre
describir lo guardado y resumir lo que vale como dato, y los conteos
—n_valores_excluidos_resumen, n_infinito_positivo,
n_faltantes_disfrazados— dicen cuánto separa a los dos.
Una columna de listas intenta contar sus valores distintos; si la clase no
admite comparación, informa NA en lugar de afirmar cero.
Las columnas compuestas —matrices, arreglos de más de una dimensión o tablas
anidadas (una columna data.frame, como la de un tibble empaquetado)— se
conservan como una unidad por fila: n informa las filas de la tabla, pero
los estadísticos por valor —n_faltantes incluido— quedan en NA y un
hallazgo explica que deben separarse en columnas con semántica explícita.
Entra por la misma puerta una columna cuyos valores no se pueden escribir
como texto —una vctrs_rcrd, que guarda varios campos por fila, o un
vctrs_vctr sin conversión declarada—: se pregunta por esa propiedad y no
por la clase, y el hallazgo tipo_compuesto_no_analizado lo dice con su
clase, en vez de abortar el perfil entero.
Un vector de bytes (raw) se compara y se publica por su escritura
hexadecimal —moda, n_distintos—, pero no pasa por las etapas que
interpretan la escritura de una persona —patrones, números escritos como
texto, unidades, espacios—: un byte no es un texto escrito.
Un nivel NA de un factor —el que dejan addNA() o factor(exclude = NULL)— es un faltante: cuenta en n_faltantes y no en n_distintos, igual
que en la medición, en distribucion_valores(), en inferir_tipo(), en
las dependencias y en las claves —detectar_claves() y sugerir_clave() no
la proponen—. Un POSIXlt —lo que deja strptime()— se perfila por su
instante, como el POSIXct equivalente: la misma columna en las dos clases
da la misma fila.
Cuando todos los valores válidos aparecen una sola vez no hay moda, y
moda queda en NA. Lo que se publicaría es el ganador de un desempate de
tantas vías como valores haya, y ese desempate sigue el orden de
ordenamiento, que depende de cómo esté guardada la columna: la misma columna
c(-5.5, -1, 0, 3.75) daba -5.5 como número y -1 como texto.
frecuencia_moda se conserva —vale 1, es cierto y es la evidencia de por qué
la moda quedó callada—. Con un solo valor distinto sí hay moda, aunque su
frecuencia sea 1: ahí no hay empate que resolver.
Una columna numérica emite valor_concentrado como señal sospechoso cuando
tiene al menos 20 valores válidos y 10 valores distintos, y su moda tiene
una frecuencia al menos cinco veces mayor que la del segundo valor más
frecuente y representa al menos 0,15 de los valores válidos. La elegibilidad
es parte de la señal: una columna con menos categorías no recibe una fila de
cobertura_diagnosticos, porque allí la moda es la distribución. La
evidencia publica el valor modal, ambas frecuencias, el cociente y la
fracción. M2 fue la regla seleccionada en
.trabajo-agente/medicion-concentracion.md: produjo cero falsos positivos en
114 columnas limpias, con un margen medido de 2,2 veces. Es una sospecha, no
una acusación: un valor legítimo puede dominar. Sus puntos ciegos medidos
son las concentraciones por debajo de 15 % y los empates naturales en
columnas enteras pequeñas, donde el cociente puede quedar por debajo de
cinco.
La ley de Benford se evalúa sólo en columnas numéricas con al menos 50
valores finitos; las columnas compuestas —matrices, arreglos de más de una
dimensión o tablas anidadas— no son magnitudes por fila y quedan fuera del
análisis. Antes de
comparar exige variación, que la columna no parezca un
identificador ni una secuencia correlativa, al menos 100 observaciones
positivas utilizables, una proporción de positivos igual a 1 y tres órdenes
de magnitud según log10(max/min). Una secuencia correlativa es una columna
de enteros que cubre al menos la mitad del tramo entre su mínimo y su máximo
sin repetirse —al menos un 90 % de valores distintos—: una magnitud
entera acotada también llena su tramo cuando se cargan filas suficientes,
pero repitiendo valores, y la exclusión no puede depender de cuántas filas
se cargaron. Es la regla de numeración de todo el perfil (ver
secuencia_entera_densa más abajo), y una clave declarada excluye a Benford
siempre, como una columna declarada en columnas_codigo. Si
falla alguna precondición no emite un
hallazgo: la enumera en cobertura_diagnosticos. Si aplica,
meta$benford$resultados conserva la distribución observada y esperada por
primer dígito, el chi-cuadrado de Pearson, ocho grados de libertad y el valor
p; meta$benford$umbrales publica todos los cortes. Un valor p menor que
0.01 genera desviacion_benford como señal descriptiva para revisar, no
como evidencia de fraude o manipulación. Topes administrativos, redondeos,
precios psicológicos y subsidios de monto fijo son explicaciones posibles.
Las relaciones aritméticas se buscan sólo entre columnas numéricas sin
clase declarada y con variación: una columna que declara una clase propia
—Date, POSIXt, difftime, integer64, units, lubridate::Period o
cualquier otra— no participa, porque sumarla o dividirla no es la aritmética
de un doble; el texto numérico y las columnas constantes tampoco. Las
Las columnas que publican cifras cuantitativas y quedan fuera por su clase
se declaran una por una en cobertura_diagnosticos: integer64, units,
lubridate::Period, haven_labelled y difftime. El criterio no es
is.numeric() —que responde FALSE sobre un difftime— sino si la fila
publica minimo, maximo y media como números: ahí la ausencia de hallazgo
se lee como conformidad y por eso hay que declararla. difftime entró en esa
categoría cuando dejó de abstenerse del resumen cuantitativo, y sin declararlo
quedaba mudo en los dos diagnósticos mientras units los declaraba —la misma
columna, con las mismas cifras, tratada de dos maneras—.
Una fecha y una hora no entran en esa declaración, y eso es deliberado:
no publican media como número pelado —su resumen sale en media_fecha y
compañía—, así que su silencio se lee distinto, y no son magnitudes para estos
diagnósticos. Quedan fuera del mismo modo que una columna de texto, sin fila
propia. meta$aritmetica_columnas publica
las clases medidas en esta tabla. La ley de Benford sigue el mismo criterio y
la misma declaración. Cada relación requiere al
menos tres filas con valores finitos en todas las columnas involucradas;
los NA, NaN e infinitos quedan fuera del universo que publica la
evidencia. Una columna integer se suma en doble: que dos enteros pasen
.Machine$integer.max no apaga la relación. Para cada terna se prueban las tres orientaciones de una identidad
aditiva; esto cubre sumas y sus restas equivalentes sin informar tres veces
la misma igualdad. En pares proporcionales, k es la mediana de los cocientes
finitos cuya base no es cero, pero el cumplimiento se evalúa después también
en las filas con base cero. Si una identidad aditiva ya relaciona una terna,
se omiten las proporcionalidades redundantes entre su total y sus sumandos;
se conserva la proporcionalidad entre los dos sumandos. Una regularidad
completa se informa con severidad "ok"; si alcanza el umbral pero tiene
discrepancias, sigue el criterio de las relaciones de orden y es
"sospechoso". Todo esto describe evidencia observada: no declara una regla
del dominio ni autoriza una corrección.
Los valores de texto que no forman UTF-8 válido tampoco se convierten: se
cuentan, se excluyen de los análisis textuales y generan un hallazgo con sus
posiciones. Los diagnósticos de invisibles incluyen controles C0/C1,
espacios Unicode, marcas direccionales, BOM y otros caracteres de transporte.
La evidencia los muestra como puntos de código; los espacios Unicode se
detectan aunque sólo se normalizan mediante una acción explícita, y ZWJ/ZWNJ
se informan pero se conservan porque pueden ser semánticos. La comparación
de duplicados con normalizar = TRUE aplica estas mismas clases sin borrar
ZWJ/ZWNJ. El resto del texto se cita con sus caracteres, y no con los
escapes de punto de código de encodeString(), que dependen del locale: la
evidencia es la misma bajo C y bajo un locale UTF-8, y el ejemplo que
cita casa con el de la tabla de patrones.
Un valor double subnormal —distinto de cero y menor que
.Machine$double.xmin en valor absoluto, unos 2,2e-308— casi siempre sale de
reinterpretar un patrón de bits o de un desbordamiento por defecto. El
hallazgo valores_subnormales los cuenta y da sus filas, con severidad
sospechoso. El caso que motiva el diagnóstico es leer una tabla donde una
columna de enteros grandes se escribió como doble reinterpretando los bits:
los números salen del orden de 1e-314 y, al venir así desde el origen,
ninguna comprobación cruzada los contradice.
No es concluyente, y el hallazgo no lo afirma. Un cálculo que desborda
por defecto cae legítimamente en ese rango —2^-1050 da 8,3e-317—, y una
columna de p-valores o de verosimilitudes muy pequeñas puede dispararlo sin
que haya nada roto. Lo que se publica es el hecho medido —cuántos valores son
subnormales y en qué filas—, no un veredicto sobre su origen.
Los resúmenes de fecha-hora se expresan siempre en UTC y llevan el sufijo
UTC en el texto para hacer visible la zona aplicada. El instante se
conserva aunque la columna de entrada use otra zona horaria. Las columnas
POSIXt declaran zona_horaria_origen y
n_filas_fecha_civil_distinta_utc, que cuenta filas cuya fecha civil cambia
al mostrar el instante en UTC. Cuando la fecha civil cambia se emite un
hallazgo zona_horaria_fecha_hora; si la zona de origen no está declarada,
el conteo queda en NA y cobertura_diagnosticos declara que no se evaluó.
Una columna de texto cuyos valores llevan el desplazamiento horario escrito
(2020-01-31T23:30:00-03:00) también publica su instante en UTC, y su fecha
civil puede no ser la escrita: el conteo es de las columnas POSIXt y queda
en NA, y cobertura_diagnosticos declara igual que no se evaluó.
La zona se declara en la columna original, por ejemplo:
attr(x, "tzone") <- "America/Montevideo".
El diagnóstico de formas Unicode compara sin modificar el texto y puede usar
el paquete opcional stringi para enriquecer la evidencia cuando existen
caracteres no ASCII. columnas$unicode_evaluado declara si esa comprobación
pudo ejecutarse; en texto no ASCII sin stringi queda FALSE,
n_variantes_unicode queda en NA y cobertura_diagnosticos informa la
dependencia ausente. Las columnas ASCII se evalúan siempre y conservan cero.
El perfil de comparación es completamente R base.
Hay un segundo caso en que unicode_evaluado queda en FALSE, y no es una
dependencia ausente: cuando todos los valores de la columna traen bytes
que no forman UTF-8 válido, no queda texto que analizar. Ahí las pasadas que
dependen de texto legible no llegan a correr, así que n_codificacion_rota,
n_codificacion_reparable, n_codificacion_reparable_parcialmente,
n_codificacion_irreparable, n_codificacion_no_se_pudo y
estado_codificacion_reparacion quedan en NA en vez de en cero: un cero
afirmaría no haber encontrado nada, y lo cierto es que no hubo nada que
mirar. n_codificacion_invalida sigue contando esos valores y el hallazgo
codificacion_invalida, de severidad error, sigue declarando que se
excluyeron.
Esas cifras no son una partición. n_codificacion_rota cuenta cada valor
afectado una sola vez; n_codificacion_reparable y
n_codificacion_reparable_parcialmente, los que el motor reparó del todo o
en parte; n_codificacion_irreparable, los que traen el carácter de
reemplazo U+FFFD, cuyo original ya no está; y n_codificacion_no_se_pudo,
los que el motor reconoce como mal convertidos y no pudo reparar. Un valor
con U+FFFD suele contar en las dos últimas, así que sumarlas no da
n_codificacion_rota.
Los nombres de columna que el perfil publica —en columnas$columna y en
todo lo que nombre una columna: claves, relaciones, dependencias, hallazgos,
patrones, el plan de remediación y el reporte— son los nombres de la tabla de
entrada tal como llegaron, con sus bytes y su marca de codificación. Eso
significa que sirven para indexar esa misma tabla —datos[[nombre]]— sea
cual sea el LC_CTYPE de la sesión. El paquete deriva una representación de
trabajo para comparar y desambiguar internamente, pero no la publica: dos
nombres con bytes distintos siguen siendo dos columnas distintas.
Al componer texto publicado —en particular las sugerencias de
posible_ausencia_estructural— usa una copia marcada como UTF-8 del nombre,
sin modificar el nombre conservado en la tabla. Por eso una sugerencia como
perfilar(datos, aplicabilidad = list(entero = ~ categoría == "categoría"))
conserva el nombre real también bajo LC_CTYPE = "C" y se puede copiar y
pegar.
La identidad de la columna no depende de esa exclusión: n_distintos,
moda y frecuencia_moda comparan la representación almacenada, que se
puede distinguir sin decodificarla. Un valor ilegible cuenta como valor, y
la moda que se publica es el valor original tal como llegó, sin
reinterpretarlo. Cuando ese valor es numérico, su representación textual se
formatea con notación fija, con la precisión completa —las cifras que hacen
falta para volver al valor: 0.1 + 0.2 se publica 0.30000000000000004,
no 0.3— e independiente de scipen y de digits, igual en
perfilar() y en los caminos de perfilar_dbi(); se conserva
OutDec, porque esa marca decimal sí pertenece a la preferencia de locale
de quien usa el paquete.
Las columnas sfc declaran su CRS, los tipos concretos y la dimensión
(XY, XYZ, XYM o XYZM), además de geometrías vacías, validez, dominio y
caja envolvente. POINT/MULTIPOINT, LINESTRING/MULTILINESTRING y
POLYGON/MULTIPOLYGON son una misma familia: el hallazgo de tipos mixtos
aparece sólo al combinar familias o ante GEOMETRYCOLLECTION.
La validez se calcula siempre con GEOS en el plano, sin CRS, para que el
resultado no dependa de que s2 esté instalado; validez_criterio publica
"planar" y n_validez_evaluados publica su universo, que incluye las
geometrías vacías porque GEOS sí devuelve su validez. Si hay una dimensión
M, se aplica sf::st_zm() y se valida la topología XY;
validez_preprocesamiento = "st_zm(x)" declara esa transformación. Si el
CRS es geográfico —con SRID mezclados, si todos lo son—, un fallo es
sospechoso y no un error, porque no afirma
invalidez esférica. Las dimensiones Z y M no se evalúan como medidas:
dimensiones_no_evaluadas las enumera y cobertura_diagnosticos deja
constancia explícita incluso cuando la validez XY sí pudo calcularse.
En un CRS geográfico el dominio exige longitudes en [-180, 180] y latitudes
en [-90, 90]. Además, las coordenadas se comparan en longitud/latitud con
la BBOX del área de uso incluida en el WKT del CRS. Este control puede
detectar valores en unidades incompatibles —por ejemplo, grados declarados
como metros—, pero no una zona UTM equivocada cuando esa interpretación cae
dentro del área de la zona declarada. Una BBOX mundial es un no-op válido;
si el WKT no permite extraer la caja, el dominio queda en NA y
cobertura_diagnosticos lo declara en lugar de suponer el mundo entero.
Si el CRS geográfico mide desde otro meridiano de origen —París, Ferro— o en
otra unidad —gradianes—, ese rango se exige en sus propias unidades y la
comparación con la BBOX se hace después de llevar las coordenadas a
longitud/latitud WGS 84. Un componente vacío de una geometría que no es
vacía —un POINT EMPTY dentro de una colección o de un MULTIPOINT— no es
una coordenada y no cuenta como fuera de dominio.
Las geometrías vacías se cuentan aparte y no integran el universo del dominio
ni de la bbox. n_dominio_evaluados y n_bbox_evaluados publican ambos
universos; la bbox se calcula sobre coordenadas crudas de geometrías no
vacías, incluidas las que el dominio marque fuera, y bbox_alcance lo
declara. Si todas son vacías, sus conteos evaluados y fuera de dominio son
cero. geometria_vacia y tipos_geometria_mixtos cuentan sobre las
geometrías medidas: un valor ausente de una columna de texto o un valor que no
se pudo convertir no es una geometría y no entra en su denominador. Sin CRS,
n_fuera_de_dominio queda en NA y se emite crs_no_declarado: nunca se
supone EPSG:4326. Si falta el paquete opcional sf, todos esos campos quedan
en NA, no se emite un hallazgo geométrico y cobertura_diagnosticos
registra la dependencia ausente. Si sf está instalado pero no se pudo cargar
en la sesión —con LC_NUMERIC en coma no carga su dependencia units—, pasa
lo mismo y el motivo lo dice: no pide instalar lo que ya está.
Una columna cuyos valores son data.frame o matriz —lo que devuelve un driver al
leer un STRUCT— declara n en filas, como cualquier otra: tiene tantos
valores como la tabla, y lo que no se puede es analizarlos como texto, que se
declara aparte en cobertura_diagnosticos.
Con un universo aplicable declarado, los conteos y los índices de geometría se
restringen a ese universo, igual que el resto del perfilado: una geometría que la
regla dejó afuera no cuenta como vacía, ni como inválida, ni como fuera de dominio,
y n_geometrias, n_dominio_evaluados y n_validez_evaluados describen el
universo. Los dos evaluados cuentan sólo lo que de verdad se evaluó dentro de
él: no entran las geometrías que el presupuesto no analizó, los valores
ausentes de una columna de texto ni, para el dominio, las geometrías vacías.
El presupuesto geométrico —los topes lupa.max_geometrias y
lupa.max_vertices, que acotan la validez y el dominio— se gasta primero en
el universo, con el mismo muestreo uniforme si el universo solo ya excede el
tope, y lo que sobra va al resto de la columna. Si aun así quedaron
geometrías del universo sin analizar, cobertura_diagnosticos dice cuántas,
junto al recorte de la columna; si el recorte cayó entero fuera del
universo, también lo dice.
Tres campos son la excepción y conviene saber por qué:
n_bbox_evaluados, n_geometrias_analizadas y n_vertices_analizados cuentan
el trabajo que se hizo sobre la columna entera —el universo y lo que el
presupuesto alcanzó del resto—, no filas de un universo: el análisis geométrico
corre sobre la columna, porque necesita su CRS y sus tipos, así que se publican
tal como se midieron. Recortarlos a ojo sería inventar un número: no hay conjunto
de índices del que derivarlos.
Una columna de texto puede ser una geometría. Se reconoce como WKT o WKB
(crudo o hexadecimal) cuando la mayoría de una muestra de 20 valores tiene esa
forma; si después hay valores que no se pueden convertir, la pérdida se declara
con su cifra en motivo_representacion y en cobertura_diagnosticos, y las
métricas cubren el resto: un valor que no convierte no borra la medición de la
columna. A sf sólo llega lo que tiene la forma de su representación —un WKT
de los tipos que el perfil mide, con su dimensión Z o M y colecciones
anidadas como las que escribe PostGIS, o un WKB de encabezado plausible—; el
texto corrupto y las curvas se declaran como no convertidos. Si los valores
declaran su SRID —SRID=n; en EWKT, o EWKB—, la columna se mide por SRID y no
se acusa crs_no_declarado, aunque ningún valor se haya podido convertir.
Esa columna se sigue perfilando como texto en lo que
toca a su escritura —espacios, codificación, invisibles—, pero no recibe los
diagnósticos que leen los valores como palabras o como claves —la proximidad de
vocabulario, posible_identificador, patron_raro—: cobertura_diagnosticos
declara que no aplican.
El argumento normalizar declara el perfil de comparación que se conserva
en meta$normalizacion; cambia sólo la representación usada para comparar,
no el texto guardado. TRUE usa el perfil predeterminado, FALSE desactiva
sus pasos configurables, "amplio" activa los tres pliegues optativos y
normalizacion() permite declararlos. También admite una lista nombrada por
columna. meta$normalizacion_fusiones informa, para cada paso activo, la
diferencia entre los valores distintos con el perfil completo y los valores
distintos con ese paso apagado y todo lo demás igual. Esa comparación
responde cuánto aporta cada paso por separado; sus números no son aditivos
porque dos pasos pueden fundir el mismo par. n_distintos y
n_distintos_normalizados declaran el total antes y después del perfil
completo. La descomposición canónica es siempre activa y forma parte de la
línea base, no una fila configurable. Un valor con marca bytes entra con su
clave de bytes —cada byte alto escrito \xHH—, como en
detectar_claves() y detectar_duplicados_aproximados(): bytes declara
que eso no se interprete como texto, así que no se pliega con el mismo texto
marcado UTF-8. Los tres cuentan igual n_distintos_normalizados.
Con normalizar = FALSE no hay pasos configurables que medir y el informe
de fusiones queda en NULL; en un perfil por columna sólo se incluyen las
columnas con algún paso activo. Si detectar_duplicados_aproximados() recibe
un perfil, puede reutilizar este informe ya calculado.
El informe usa el vocabulario completo: n_distintos y n_usados son el
número de valores distintos realmente comparados y el estado es exacto.
Las fusiones son una propiedad de pares, por lo que muestrear valores
aislados podría dejar fuera los dos miembros de cada par y convertir una
fusión real en un cero falso. La normalización se vectoriza para que este
alcance completo no dependa de la cardinalidad de la columna.
Las columnas sin ausentes que tienen al menos 100 filas y 90 % de valores
distintos producen un hallazgo casi_clave cuando el valor dominante
concentra al menos la mitad de los duplicados excedentes. La concentración
se calcula sobre las repeticiones posteriores a la primera de cada valor, no
sólo sobre la tasa de distintos. La evidencia declara el mínimo de filas,
ambos umbrales, los valores que colisionan y sus frecuencias. Las variables
con rol propuesto fecha, incluidas fecha-hora, se excluyen. Así una colisión
concentrada queda separada del texto libre de alta cardinalidad con
repeticiones dispersas.
Un vector double sólo participa si todos sus valores finitos son enteros;
así se conservan identificadores importados con ese almacenamiento y se
excluyen medidas con alguna parte fraccionaria, como importes o coordenadas.
Los vectores integer64 se tratan como enteros semánticos. La evidencia del
hallazgo declara este criterio y el recuento observado.
Además, si casi_duplicados_vocabulario = TRUE, el perfil busca variantes
casi duplicadas en columnas de texto. Agrupa el vocabulario crudo mediante
fusiones exactas de la normalización y estrellas de distancia centradas en
un valor de frecuencia estrictamente mayor y único; los empates no se fuerzan.
No cierra cadenas transitivamente ni elige una forma canónica. La unidad es
el valor distinto, no la fila, y cada variante
conserva su frecuencia. Cada grupo declara sus distancias mínima y máxima.
El límite max_proporcion_grupo_vocabulario evita presentar un grupo que
abarque casi toda la columna como un diagnóstico útil: en ese caso el alcance
dice que el diagnóstico no aplica. Ese límite se activa desde 20 valores
distintos o cuando el grupo mayor ya tiene 10 variantes; sólo suprime el
grupo si además ocupa una fracción mayor que el umbral. Así un grupo de tres
en cuatro valores se entrega, pero quince variantes que ocupan toda una
columna no se presentan como una sola familia. Un grupo grande dentro de un
vocabulario mucho mayor puede seguir pasando si su proporción es pequeña. El
alcance expone ambos cortes. El argumento permite apagar el detector cuando
no corresponde a la tabla. Si hay pares cercanos pero todas las frecuencias
empatan, el alcance declara que no hubo asimetría para formar una estrella y
sugiere detectar_duplicados_aproximados() para comparar filas. El alcance
clasifica cada grupo como normalizacion_exacta, dentro_de_palabra,
token_completo, token_unico, mixta o indeterminada. La primera indica
una coincidencia tras normalizar; dentro_de_palabra, una diferencia dentro
de un token; token_completo, diferencias en tokens completos;
token_unico, que ambos valores son un único token y la clase estructural no
aplica; mixta, aristas de más de una clase; e indeterminada, que no hubo
aristas clasificables. Son evidencia descriptiva, no una decisión sobre
identidad. El agrupamiento no cambia por esa etiqueta. El alcance
también descarta aristas de distancia cuyos números no coinciden. Se comparan
las secuencias numéricas, quitando ceros de relleno y separadores de miles;
una diferencia numérica se trata como otra entidad. Esto puede dejar sin
agrupar una errata dentro de un número, porque no hay evidencia para
distinguirla de dos entidades reales. El alcance declara los valores y pares
comparados, los recortes por cardinalidad y si stringdist no estuvo
disponible; también informa cuántas aristas se descartaron por esa regla y
el tamaño compatible después del filtro. El tamaño máximo usado para el
límite conserva el componente potencial antes del filtro numérico, para que
una familia de entidades numeradas no vuelva a presentarse como una sola
variante; el tamaño compatible se muestra aparte. Para mantener acotado el
perfil, por omisión se evalúan hasta
5.000 valores distintos y 2.000.000 de pares de unidades normalizadas; si
se alcanza un límite, la evidencia lo declara y no presenta el resultado
como universo completo. Las fusiones exactas se informan aun sin ese paquete
opcional.
Antes de formar esos grupos se retiran los valores que el mismo perfil ya
informó como faltantes_disfrazados. El diagnóstico fuerte de ausencia tiene
precedencia: un centinela no se presenta también como posible variante de un
valor válido. El alcance declara cuántas observaciones retiró este filtro.
La corazonada de centinelas numéricos se apaga sobre una numeración
limpia, para no llamar ausencia a un código válido. densa responde a la
cobertura del rango —densidad_secuencia_entera por encima de su umbral— y
a que los valores casi no se repitan: al menos un 90 % de valores
distintos, descontando el excedente de la moda. Cubrir el rango sola lo
cumple también una magnitud entera acotada cuando se cargan filas
suficientes, y el veredicto dependía de cuántas filas se leyeron; una
numeración que se repite —un código de catálogo, una clave foránea— no se
distingue por su forma de esa magnitud y no se trata como numeración. Es la
misma regla que deciden posible_identificador, la guarda de los límites de
Tukey, la de los patrones raros y la de Benford. Por eso un código de
catálogo que se repite —un EstCod de 1 a 284— recibe límites de Tukey y,
con tres órdenes de magnitud, Benford, aunque se lo pase como texto o como
factor: el paquete infiere número. Para eso está columnas_codigo.
moda_sobresale_secuencia_entera es una señal independiente y no
cambia los tres escudos de forma que dependen de la numeración.
En una columna integer64, bit64 se carga de manera diferida si está
instalado para registrar sus métodos antes de medir. Si no está instalado,
las cinco medidas públicas de la secuencia quedan en NA y
cobertura_diagnosticos declara que secuencia_entera no se evaluó por
falta de esa dependencia.
Lo mismo pasa si la columna tiene valores por encima de 2^53: en un doble se
redondean, la densidad no se mide sobre números redondeados, y la secuencia
queda en NA y declarada, no como «no densa».
La guarda vuelve a abrirse si un candidato presente queda fuera del rango de
los valores restantes de la numeración, o si la frecuencia del candidato es
la moda sobresaliente. Esta segunda señal se mide por el salto entre
frecuencias consecutivas, ordenadas de mayor a menor y mirando sólo las
primeras posiciones. Es el mismo idioma que salto_de_escala_secuencia_entera
usa con los huecos: lo que delata a un centinela no es que su frecuencia sea
grande, sino que haya un acantilado entre el grupo que sobresale y el resto
de la distribución.
Comparar contra un valor concreto no alcanza, y cada intento lo rompió un caso distinto: contra el segundo valor, un señuelo legítimo y frecuente infla el segundo puesto; contra la frecuencia típica exigiendo forma de numeración, un centinela masivo sobre una clave foránea nunca la tiene; y con las dos juntas, varios centinelas empatados se cubren entre sí —tres valores con la misma frecuencia dejan «máximo igual al segundo» y la comparación vacía—. El acantilado ve los tres porque no le importa quién es el segundo sino dónde se corta la distribución.
Se miran sólo las primeras posiciones porque el grupo que sobresale está arriba: en la cola, una caída de dos apariciones a una da un salto que no dice nada de la columna. Medido sobre las diez columnas numéricas del banco real y dos formas de clave foránea, ninguna pasa de 1,71; los casos que hay que atrapar van de 4,75 a 249. Perder la condición de numeración no genera un hallazgo por sí solo: sólo devuelve la columna a la mirada de la lista de centinelas. En una numeración compacta, un candidato fuera de rango también abre la guarda aunque su frecuencia no forme un acantilado.
El factor de frecuencia sólo califica la señal moda_sobresale: una caída
menor no abre esa vía, pero tampoco bloquea la vía independiente del rango.
Así, un candidato fuera de rango se informa aunque aparezca cuatro veces y
no forme un acantilado. Lo que el usuario declara con sentinelas_numericos
atraviesa la guarda y se informa con severidad error, que es la regla
general del paquete: excluye lo que se declara e incluye lo que sospecha.
La clasificación de posibles datos personales es más amplia que la
protección. Cada clasificación declara poder_discriminante y proteger:
debil: una forma genérica, como siete a doce dígitos, coincide también
con importes, facturas y códigos; se informa y no se ocultan su moda, sus
ejemplos ni sus estadísticos. En la evidencia de sus hallazgos se tapan
sus valores de seis caracteres o más —la colisión de una casi clave—, no
la evidencia entera: hasta la ronda 26 se tapaba entera, también la que no
cita ningún valor ("4939 valores distintos de 5000 (0.988)", "3 valores"); sin la tabla —un perfil que se vuelve a proteger al
guardarlo— no hay valores con que barrer y se tapa entera;
medio: el nombre de la columna expresa una categoría personal (por
ejemplo telefono, fecha_nacimiento o fecha_fallecimiento); se protege aunque sus valores no
se puedan validar. El nombre tiene prioridad sobre una forma numérica
genérica y también determina la etiqueta de tipo. Una palabra de nombre
—nombre, apellido— la expresa en cualquier lugar del nombre:
nombre_cliente es un nombre, y también nombre_jefe_grupo o
nombre_grupo; una columna de números o de fechas no lo es
(cantidad_nombres). Una palabra de persona —cliente,
socio, usuario, persona, paciente, titular— dice de quién
habla la columna, no que guarde su nombre: la clasifica como nombre
sólo si ninguna otra palabra del nombre la clasificó —direccion_cliente
es un domicilio—, si el resto del nombre no dice que es otra cosa —una
clave (cliente_id, id_usuario, cod_socio), un tipo, un estado o una
categoría (tipo_cliente, estado_socio), una fecha (cliente_desde,
fecha_alta_socio), una cantidad o un importe— y si sus valores pueden
ser nombres: una columna de números, fechas o lógicos no lo es. Hasta la
ronda 33 bastaba la palabra, y esas columnas se protegían como nombres:
perdían su moda y sus extremos, y sus categorías —"mayorista",
"empresa"— se tapaban también en las demás columnas. La misma regla vale
para las demás palabras: tipo_documento es una categoría y no un
documento, fecha_contacto y medio_contacto no son teléfonos,
estado_correo no es un correo y tipo_domicilio no es un domicilio;
sí lo siguen siendo nro_documento, documento_id, numero_celular,
telefono_contacto, barrio_id, segundo_nombre o una fecha de
nacimiento guardada como número. Y una palabra que el resto del nombre
califica se decide con sus valores, con el tipo que ellos muestran y
no el de la palabra, en el 80 % de sus valores distintos: un correo —una
@, también el de una intranet: correo_alta con "jperez@intranet"—;
un teléfono de siete a quince cifras escrito como teléfono —con +, con
un cero inicial, con separadores, con un interno o en una lista:
responsable_canal con "+598 99 123 456"—, sin centavos y en una
columna que no es de fechas, y con la palabra del teléfono también las
cifras sueltas (celular_estado); un domicilio con una marca de calle
—Av., calle, Bvar., ruta, esq., apto— en al menos el 30 % de
sus distintos (cliente_tipo con "Av. Italia 2345"); o un nombre con
un nombre de pila conocido, sin cifras, sin una palabra que no va en un
nombre —sin, en, por— y sin empezar como un lugar —San,
Villa—, aunque sean dos personas (responsable_grupo con
"Perez Joaquin", contacto_alta con "Ramon Acosta"). La forma sola
no alcanza: "Gran Empresa", "Cerro Largo", "Arroz Premium" u
"Oficina 101" siguen siendo categorías. Y la palabra de quien actúa en
el registro —usuario, funcionario, responsable, solicitante— con
una de auditoría —alta, baja— dice quién dio el alta o la baja: ahí
basta un texto que no sea una fecha —un login "jperez", un apellido
solo—; la de quien es objeto del registro pide la señal de los valores,
y cliente_desde con "Sin fecha" o paciente_alta con "Traslado" se
publican. En la ronda 34 el nombre decidía solo, y esas columnas
publicaban nombres, celulares y domicilios. Limitación conocida: la lista
de nombres de pila no es completa, y un nombre que falta deja la columna
sin esa señal; una localidad con nombre de persona —"Jose Ignacio",
"Juan Lacaze"— se lee como un nombre; y con una palabra de categoría o
de clave, un login de sólo letras o un apellido solo
(responsable_grupo con "Rodriguez", usuario_id con "jperez_01")
no se distinguen de una categoría o de un código y se publican, como un
domicilio sin marca de calle. Una columna de
personas con otro nombre se declara en columnas_personales. También es medio una
columna que trae correos completos sin que sean la mayoría de sus valores:
un correo es un correo aunque esté solo, y la columna se protege;
alto: una forma muy específica, como un correo, o nombre y forma se
apoyan mutuamente; se protege;
verificado: al menos tres valores distintos y al menos el 90% cumple uno
de los validadores personales configurados; se protege incluso sin un
nombre orientador. La proporción medida —la que se compara contra ese
umbral— se publica en proporcion_verificada, para que una columna con el
90% no se lea igual que una con el 100%. El pack uruguayo es el predeterminado, pero puede
reemplazarse por un pack_validadores() de otro país o desactivarse con
FALSE. La tolerancia del 10% permite tipeos aislados sin convertir una
columna real en una salida pública; el umbral es configurable.
La forma genérica de siete a doce dígitos tiene poder discriminante débil: también describe importes, teléfonos, facturas e identificadores. Las formas con separadores sólo se aceptan cuando tienen una estructura de documento reconocible (por ejemplo, una cédula con grupos y guion o un RUT con grupos de tres y cuatro dígitos); una fecha ISO, una fecha con puntos o guiones y separadores arbitrarios no se consideran documentos. Un validador de dígito que supera el umbral aporta evidencia verificable y eleva la clasificación; una forma sola nunca se trata como prueba de identidad.
Este criterio mide capacidad de discriminación, no juzga si la presencia del
dato es correcta. La protección sustituye modas, ejemplos, evidencia y
extremos o medianas que corresponden a observaciones reales. También los
momentos: las medias en sus dos formas (media y media_fecha), porque la
media de una columna personal puede reconstruir demasiado, y el desvío, que
es una magnitud de sus valores —hasta la ronda 32 se conservaba, por una
decisión declarada que se revirtió—. detalle_proteccion_personal hace
visible la supresión y dice qué se tapó: los estadísticos de orden, la media,
el desvío o los dos momentos. En fechas de
nacimiento y fallecimiento, un hallazgo separado conserva el diagnóstico de
valores anteriores a 1900 o posteriores a la corrida sin publicar las fechas.
Los números escritos como texto reconocen tanto coma como punto decimal y
sus separadores de miles simétricos. El resumen cuantitativo, los outliers y
la secuencia entera los leen con la convención que la misma fila publica en
numero_texto_convencion, por la misma normalización que la acción
convertir_numero_regional: con "decimal_coma", 1.500 es mil quinientos.
Un separador seguido de exactamente tres dígitos —1.500, 1,500— sólo se
lee si la columna aporta evidencia de una sola convención; si no la aporta
("ambigua") o aporta de las dos ("mixta"), ese valor no se lee como
número, con punto o con coma por igual: una columna entera de esos valores
queda texto y sin resumen, y si son pocos quedan fuera del resumen y lo
declaran n_valores_excluidos_resumen y "calculados_sobre_valores".
Los prefijos de tres letras separados
del número, incluso como sufijo, U$S y los símbolos monetarios se
conservan como evidencia; monedas_mixtas informa sus frecuencias sin
convertir ni suponer tasas de cambio. Una única moneda o un símbolo $
aislado no produce ese hallazgo.
Un sufijo de unidad se reconoce sólo si es % o una abreviatura alfabética
en minúsculas; por eso 12 kg y 13500 g son unidades, mientras que
12A y 13B se tratan como códigos. Si hay más de una unidad observada,
unidades_mixtas informa sus frecuencias y no convierte ni compara sus
magnitudes. Una única unidad no genera ese hallazgo.
formatos_fecha_mixtos compara representaciones dentro de una columna cuyo
dominio es la fecha o la fecha-hora. Una fecha agregada adentro de una
columna de horas del día —12/02/2011 6:55 a.m. entre valores 7:10 a.m.—
no produce ese hallazgo, porque la columna no es de fechas: aparece como
patron_raro, que es lo que de verdad cambió. Decidir que esa columna sólo
debe contener horas es una regla de dominio y el paquete no la supone.
Del mismo modo, un número sin sufijo no cuenta como una segunda unidad: una
columna que mezcla 5 con 5 % observa una sola unidad y se informa como
numero_como_texto, no como unidades_mixtas.
celdas_multivaluadas es deliberadamente conservador: usa los patrones de
descubrir_patrones() y exige partes numéricas, alfanuméricas o
identificadoras puntuadas homogéneas, compatibles con el patrón del resto de
la columna. No interpreta comas en nombres o direcciones como listas; el
delimitador, la cantidad de celdas y la distribución de valores por celda
quedan en la evidencia del hallazgo.
descubrir_patrones(), detectar_dependencias(),
proponer_modelo(), planificar_limpieza()
perfil <- perfilar(datos_administrativos) perfil summary(perfil)perfil <- perfilar(datos_administrativos) perfil summary(perfil)
Recorre las tablas declaradas en coleccion() y devuelve una fila por
tabla con su resumen exacto, más la cobertura de lo que no se pudo medir o
no se solicitó.
perfilar_coleccion( coleccion, muestra = Inf, conservar_perfiles = FALSE, cobertura_metricas = c("no_medidas", "completa", "ninguna"), tope_cobertura_metricas = 20000L, bloque_muestra = c("con_muestra", "solo_agregados"), ... )perfilar_coleccion( coleccion, muestra = Inf, conservar_perfiles = FALSE, cobertura_metricas = c("no_medidas", "completa", "ninguna"), tope_cobertura_metricas = 20000L, bloque_muestra = c("con_muestra", "solo_agregados"), ... )
coleccion |
Objeto creado por |
muestra |
Filas solicitadas por tabla para el bloque en memoria. Por
omision |
conservar_perfiles |
Si se retienen los objetos |
cobertura_metricas |
Qué se sube a |
tope_cobertura_metricas |
Máximo de filas de |
bloque_muestra |
Qué bloques se solicitan en cada tabla: |
... |
Argumentos enviados a |
Conserva el resumen calculado en SQL sobre toda la tabla, que es acotado, y
no retiene el perfil de la muestra de cada tabla, que es el objeto
pesado: con cientos de tablas eso no entra en memoria. conservar_perfiles
permite retenerlos cuando la colección es chica y se los necesita.
Cada fila de cobertura_coleccion nombra la tabla con las tres partes de su
identidad —catalogo, esquema y tabla—, las mismas que coleccion()
declaró. Es lo que permite que el motivo siga pegado a la tabla cuando el
número sube de nivel: sin el catálogo, una tabla declarada como
catalogo.esquema.tabla se publicaba río abajo como esquema.tabla.
Lo que no se pudo medir se declara, y nunca a cero. Una tabla que la
credencial no puede leer, un objeto que no es una tabla base y una tabla
vacía van a cobertura_coleccion con su motivo y su como_resolverlo. Un
motor que rechaza un agregado —o una columna cuyo tipo no lo admite— va a
cobertura_metricas, con una fila por columna y métrica, y deja además una
línea de resumen en cobertura_coleccion. Esa cobertura por métrica se
calcula antes de descartar el perfil, así que existe también con
conservar_perfiles = FALSE. En bases institucionales los permisos parciales
son el caso normal, no el borde.
Donde no hubo medición va NA, no 0. Una tabla vacía deja
prop_faltantes en NA en todas sus columnas: antes eso producía
prop_faltantes_maxima = -Inf —fuera del [0, 1] que el paquete promete, y
ordenada como la tabla de mejor calidad de la base— y
n_columnas_sin_faltantes = 0, que afirmaba tres columnas sin ausencias
sobre una tabla de la que no se sabía nada. n_columnas_medidas declara
sobre cuántas columnas se conoce la proporción de ausentes, para que los dos
números anteriores se lean con su alcance.
No hay lectura instantánea. Perfilar una colección son muchas consultas y
la base puede cambiar entre ellas: una tabla puede truncarse después de que se
contaron sus filas. Por eso cada fila declara el momento en que se midió y
meta$snapshot declara que no lo hubo.
Cada fila de resumen_coleccion trae universo, que nombra el universo de
las metricas SQL que alimentan sus agregados. En particular, con
universo = "muestra_motor", n_filas sigue siendo el conteo de la tabla
completa pero las proporciones observadas declaran que provienen de la
muestra, en vez de presentarse como cifras de la tabla entera.
meta$lecturas conserva el SQL emitido por las tablas perfiladas, con su
identidad, aunque conservar_perfiles = FALSE. sql_perfil() lo devuelve
como una tabla unica; los perfiles completos siguen disponibles por tabla
cuando se pidio conservarlos.
Limitación conocida: las declaraciones por columna valen para todas las
tablas. columnas_codigo, columnas_opcionales, columnas_personales y
las demás llegan por ... iguales a cada perfilar_dbi(), y coleccion() no
tiene declaraciones por tabla. Una tabla que no tiene una columna declarada
en columnas_codigo no se perfila —sale de resumen_coleccion y
cobertura_coleccion nombra la causa, «columnas_codigo nombra columnas
inexistentes», aunque como_resolverlo hable de permisos—; con
columnas_opcionales o columnas_personales la tabla conserva su resumen y
pierde la muestra. columnas_sin_ceros y columnas_no_negativas no la
afectan. Para declarar columnas que no están en todas las tablas, perfilar
esas tablas por separado o partir la colección.
Objeto de clase perfil_coleccion con resumen_coleccion,
cobertura_coleccion, cobertura_metricas, meta y, si se pidió,
perfiles.
if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) perfilar_coleccion(coleccion(con, "personas", nombre = "padron")) DBI::dbDisconnect(con) }if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:3, nombre = letters[1:3])) perfilar_coleccion(coleccion(con, "personas", nombre = "padron")) DBI::dbDisconnect(con) }
Calcula en SQL un resumen sobre la tabla completa o sobre una relación
muestreada por el motor, según universo. Con universo = "muestra_motor"
y bloque_muestra = "con_muestra", ejecuta una sola selección y la
materializa en un spool externo de la sesión cliente; el resumen y
perfilar() leen esa misma materialización. Con
bloque_muestra = "solo_agregados", el resumen usa la relación muestreada
sin seleccionar filas para el cliente.
bloque_muestra = "solo_agregados" sigue omitiendo el objeto
perfil_muestra, pero no vuelve a seleccionar filas.
perfilar_dbi( conexion, tabla, universo = c("tabla_completa", "muestra_motor"), muestra_motor = NULL, muestra = Inf, orden_muestra = NULL, metricas = .METRICAS_DBI, estrategia_distintos = "exacta", estrategia_mediana = c("exacta", "aproximada_motor"), politica_costo = c("todas", "por_cardinalidad"), bloque_muestra = c("con_muestra", "solo_agregados"), max_consultas = Inf, dialecto = "auto", incluir_valores = TRUE, tamano_lote = NULL, tamano_lote_planos = .TAMANO_LOTE_PLANOS_DBI, tamano_lote_distintos = .TAMANO_LOTE_DISTINTOS_DBI, instrumentar = TRUE, umbral_cardinalidad = .UMBRAL_CARDINALIDAD_COSTO_DBI, avisar_costo_distintos = TRUE, umbral_segundos_aviso_distintos = .UMBRAL_SEGUNDOS_AVISO_DISTINTOS_DBI, avisar_costo_moda = TRUE, umbral_segundos_aviso_moda = .UMBRAL_SEGUNDOS_AVISO_MODA_DBI, avisar_costo_mediana = TRUE, umbral_segundos_aviso_mediana = .UMBRAL_SEGUNDOS_AVISO_MEDIANA_DBI, avisar_derrame_estimado = TRUE, umbral_bytes_aviso_derrame_estimado = .UMBRAL_BYTES_AVISO_DERRAME_ESTIMADO_DBI, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, bloque_filas = NULL, max_bytes_procesamiento = .MAX_BYTES_MUESTRA, max_bytes_materializacion = .MAX_BYTES_MUESTRA, columnas_opcionales = character(), aplicabilidad = NULL, sentinelas_numericos = c(-9, -99, -999, -9999, 999), ... )perfilar_dbi( conexion, tabla, universo = c("tabla_completa", "muestra_motor"), muestra_motor = NULL, muestra = Inf, orden_muestra = NULL, metricas = .METRICAS_DBI, estrategia_distintos = "exacta", estrategia_mediana = c("exacta", "aproximada_motor"), politica_costo = c("todas", "por_cardinalidad"), bloque_muestra = c("con_muestra", "solo_agregados"), max_consultas = Inf, dialecto = "auto", incluir_valores = TRUE, tamano_lote = NULL, tamano_lote_planos = .TAMANO_LOTE_PLANOS_DBI, tamano_lote_distintos = .TAMANO_LOTE_DISTINTOS_DBI, instrumentar = TRUE, umbral_cardinalidad = .UMBRAL_CARDINALIDAD_COSTO_DBI, avisar_costo_distintos = TRUE, umbral_segundos_aviso_distintos = .UMBRAL_SEGUNDOS_AVISO_DISTINTOS_DBI, avisar_costo_moda = TRUE, umbral_segundos_aviso_moda = .UMBRAL_SEGUNDOS_AVISO_MODA_DBI, avisar_costo_mediana = TRUE, umbral_segundos_aviso_mediana = .UMBRAL_SEGUNDOS_AVISO_MEDIANA_DBI, avisar_derrame_estimado = TRUE, umbral_bytes_aviso_derrame_estimado = .UMBRAL_BYTES_AVISO_DERRAME_ESTIMADO_DBI, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, bloque_filas = NULL, max_bytes_procesamiento = .MAX_BYTES_MUESTRA, max_bytes_materializacion = .MAX_BYTES_MUESTRA, columnas_opcionales = character(), aplicabilidad = NULL, sentinelas_numericos = c(-9, -99, -999, -9999, 999), ... )
conexion |
Conexión abierta compatible con DBI. |
tabla |
Nombre de tabla o un objeto aceptado por
|
universo |
Universo sobre el que se calculan los agregados SQL:
|
muestra_motor |
Cantidad positiva y finita de filas que el motor debe
tomar cuando |
muestra |
Cantidad positiva de filas solicitadas para el perfil de
muestra que se trae a R, o Sin |
orden_muestra |
Columnas para |
metricas |
Selección explícita de grupos de métricas: |
estrategia_distintos |
Procedencia explícita para |
estrategia_mediana |
Preferencia para resolver |
politica_costo |
Política optativa para las métricas caras. El valor por
omisión, |
bloque_muestra |
Qué bloques se solicitan: |
max_consultas |
Presupuesto declarado de consultas. Al agotarse, las
métricas restantes quedan en |
dialecto |
Capacidad de acotar filas: |
incluir_valores |
Si el resumen informa valores de celda: moda, mínimo,
máximo y mediana. Con |
tamano_lote |
Cantidad máxima de columnas por consulta consolidada.
Se conserva por compatibilidad y, si se informa, fija el tamaño de las dos
familias. Para control separado, usar |
tamano_lote_planos |
Cantidad máxima de columnas por consulta de agregados planos. El valor por omisión es 20. |
tamano_lote_distintos |
Cantidad máxima de columnas por consulta de
cardinalidades exactas. El valor por omisión es 2, medido sobre el servidor
de referencia: el |
instrumentar |
Si se cronometra cada consulta y las etapas grandes de R
y, en PostgreSQL, se intenta atribuir el uso de bloques temporales de los
|
umbral_cardinalidad |
Proporción entre valores distintos y válidos que
activa la omisión de la moda con |
avisar_costo_distintos |
Si es |
umbral_segundos_aviso_distintos |
Segundos estimados a partir de los
cuales se emite el aviso del costo de |
avisar_costo_moda |
Si es |
umbral_segundos_aviso_moda |
Segundos estimados a partir de los cuales
se emite el aviso de la moda. Por omisión es |
avisar_costo_mediana |
Si es |
umbral_segundos_aviso_mediana |
Segundos estimados a partir de los cuales
se emite el aviso de la mediana. Por omisión es |
avisar_derrame_estimado |
Si es |
umbral_bytes_aviso_derrame_estimado |
Tamaño estimado del hash, en
bytes, a partir del cual se avisa un derrame potencial entre los lotes que
ya superan la memoria efectiva. Por omisión es |
max_celdas_muestra |
Máximo de celdas que puede contener el bloque
|
max_bytes_muestra |
Máximo de bytes de la muestra materializada que
alimenta |
bloque_filas |
En la via I1, entero positivo que activa el recorrido de
|
max_bytes_procesamiento |
Límite de bytes del estado retenido por el
procesamiento por bloques y por la lectura del spool en R. Se comprueba
antes de publicar un bloque o una pasada; |
max_bytes_materializacion |
Presupuesto de bytes del spool externo de
|
columnas_opcionales |
Nombres de columnas que sólo son aplicables a
parte de las filas. Esta política se aplica al |
aplicabilidad |
Lista nombrada de fórmulas de un solo lado, con una
regla por columna. Se reenvía al |
sentinelas_numericos |
Vector completo de valores numéricos que se
declaran como faltantes en el Rige la misma reserva que en |
... |
Argumentos enviados a |
Antes, la promesa era literalmente: "la función no escribe en la conexión ni
crea objetos temporales". Ahora es: "no escribe en la conexión ni crea
objetos temporales del motor; muestra_motor puede crear un spool externo
temporal de sesión, con bytes, checksum, presupuesto y estado". DBI es una
dependencia opcional. Cada agregado no disponible queda en NA y su
consulta, estado y motivo se conservan en resumen_tabla$sql.
Las expresiones se ejecutan como capacidades a comprobar, no como un
dialecto SQL universal.
Objeto de clase perfil_dbi con dos componentes: resumen_tabla, de
alcance completo o muestreado según universo, y perfil_muestra, un objeto
perfil cuyo meta$origen_dbi declara tabla, conexión, SQL y alcance.
perfil_muestra es NULL si la muestra no se pudo obtener o si se pidió
bloque_muestra = "solo_agregados"; resumen_tabla$cobertura distingue
esos casos con no_disponible y no_solicitado, respectivamente.
Cuando universo = "muestra_motor" y no se solicitó el bloque, la metadata
además declara meta$materializacion$estado = "no_solicitado" y
meta$bloques$filas_vistas = 0: las filas no se materializaron y luego se
descartaron.
resumen_tabla$meta$clave conserva siempre la respuesta del catálogo de la
clave primaria: columnas, fuente, motivo, garantia y estado.
garantia puede ser garantizada, declarada_no_garantizada,
desconocida o no_declarada; estado conserva, cuando el motor los
expone, visible, restriccion_diferible,
universo_incluye_descendientes e indice_no_unico. Una consulta fallida
queda diferenciada de una clave no declarada.
El resultado trae dos, y cubren cosas distintas. resumen_tabla$cobertura
habla de métricas SQL: qué pidió esta función al motor y qué pasó, con
bloque, elemento, estado —no_disponible, no_solicitado,
degradado, presupuesto_agotado, alcance_distinto— y la consulta en
sql. alcance_distinto declara que dos valores exactos incoherentes
salieron de grupos de consistencia distintos: es evidencia de que la tabla
cambio durante la corrida, no una acusacion contra el motor.
perfil_muestra$cobertura_diagnosticos habla de diagnósticos: qué
comprobación no se corrió sobre la muestra y por qué, con diagnostico,
columna, motivo y como_resolverlo, el mismo esquema que devuelve
perfilar(). Un motor que rechaza una columna aparece en la primera; una
prueba estadística que no corresponde a esa columna, en la segunda. Comparten
la palabra y no el vocabulario, así que conviene mirar cuál se está leyendo.
Si una columna declarada como entero de 64 bits o mas ancho, o como decimal
-numeric, decimal, con o sin precision-, llega como doble
y contiene un valor con valor absoluto de al menos 2^53, resumen_tabla
deja sus metricas de magnitud en no_disponible. Las siete: minimo,
maximo, media, n_ceros, n_negativos, mediana y desvio. La condicion
es de la columna y no de la metrica, aunque las dos ultimas se calculen en otras
consultas; lo que el motor cuenta con exactitud -n_distintos, la moda- se sigue
publicando. Lo decide el valor: un decimal desde 2^53 ya no cabe en un doble
-90071992547409001 y 90071992547409002 son el mismo-, y el tipo solo dice
que viene de un decimal: RPostgres lo publica como numeric sin precision y
odbc con su codigo ODBC (2 y 3), asi que una regla que esperara
numeric(20,0) no se ejerceria. Para la muestra, la
seleccion SQL trae esas columnas como texto y, si bit64 esta instalado,
las convierte a integer64 antes de perfilar: n_distintos, la moda y los
diagnosticos por identidad conservan los valores exactos. Los estadisticos
de magnitud por encima de 2^53 siguen usando
estado_resumen_cuantitativo = "omitidos_precision", como en memoria. Si
falta bit64, la columna queda como texto y cobertura_diagnosticos explica
la limitacion y recomienda instalarlo o pedir integer64 al conectar. Las
columnas declaradas como double, real o float no entran en esta guarda.
Con bloque_filas rige la misma regla, sobre los valores que el
controlador entrega como integer64 —un BIGINT o bigserial de
PostgreSQL, un INTEGER de SQLite que no entra en 32 bits—: por debajo de
2^53 se opera con el valor exacto, y desde 2^53 las siete métricas quedan
no_disponible con su motivo. Un decimal o un entero ancho que llega como
doble desde 2^53 deja ademas n_distintos, tasa_distintos, la moda y su
frecuencia en no_disponible: con bloques se contarian en R sobre esos
dobles, que no distinguen los valores. Las cifras de perfil_muestra de esa
columna salen de los mismos dobles; sin bloques, la corroboracion avisa la
diferencia en n_distintos y la moda contra las del motor. Un decimal por
debajo de 2^53 con mas cifras de las que guarda un doble -numeric(30,20)-
no entra en esta guarda. Ninguna columna cambia la clase de un campo
de la tabla: minimo, maximo, media, mediana y desvio de
resumen_tabla$columnas son siempre dobles.
Si se omite el bloque con bloque_muestra = "solo_agregados", la cobertura
usa el estado no_solicitado: no es un fallo ni se cuenta como una métrica
no disponible. En muestra_motor, la selección materializada se hace una
sola vez antes de las pasadas; el spool se relee y se verifica mediante su
trailer. Un trailer ausente publica
spool_incompleto:trailer_ausente; un trailer o checksum que no coincide
publica spool_checksum_invalido.
Ningún bloque descarta al otro. Si se pide la muestra pero el motor rechaza su
consulta, o si la muestra no se puede perfilar, el resultado sale igual con
perfil_muestra = NULL y una fila en resumen_tabla$cobertura que declara
el estado no_disponible, el motivo y cómo resolverlo. Si se pide sólo
agregados, perfil_muestra = NULL se acompaña de una fila no_solicitado:
no se intentó leer la muestra. Si el motor rechaza una columna, esa columna
queda con sus métricas en no_disponible y las demás se perfilan enteras.
Lo mismo con una columna cuyo nombre la sesión no puede escribir: odbc pasa
la consulta por la codificación nativa, y bajo el locale C —el de un
Rscript de cron en Linux— un nombre con tilde no se puede mandar; mandarlo
dejaba la conexión rota y la consulta siguiente hacía caer a R. La consulta
no se envía: la columna queda en no_disponible con ese motivo, y una tabla
con un nombre así da lupa_error_codificacion_dbi. Para leerlas, correr la
sesión con un locale UTF-8 (LC_ALL=C.UTF-8). RPostgres, RSQLite y duckdb
reciben la consulta en UTF-8 y leen esos nombres en cualquier locale.
Una columna que la credencial no puede leer —un GRANT SELECT por columnas o
un DENY SELECT sobre una columna— se sabe antes de leer, por el catálogo del
motor: en PostgreSQL pg_attribute con has_column_privilege(), legibles
por cualquier rol (dbListFields() de RPostgres no lista esas columnas), y en
SQL Server sys.columns con HAS_PERMS_BY_NAME(). La columna aparece en
resumen_tabla$columnas con sus métricas en no_disponible y una fila de
cobertura con el motivo y el remedio, no se nombra en ninguna consulta, y las
demás se perfilan enteras. SQL Server rechaza además cualquier consulta sobre
la tabla, también COUNT(*), cuando hay una columna sin permiso: la lectura
va sobre una tabla derivada con las columnas legibles, y el SQL publicado lo
muestra. Con universo = "muestra_motor" la columna se declara igual, en
resumen_tabla$columnas y en la cobertura; TABLESAMPLE no se puede
escribir sobre la tabla derivada, así que la muestra del motor usa otra forma
que el motor acepte o se degrada con su motivo (limitación conocida). Con odbc las consultas se ejecutan sin preparar
(immediate = TRUE, como el dbGetQuery() de odbc): preparada, una consulta
rechazada por un permiso de columna dejaba la conexión rota y la siguiente
hacía caer a R. Lo que el catálogo no ve —un sinónimo, una tabla de otra
base, una tabla temporal— se lee como antes: el rechazo del motor se declara
y la conexión sigue.
Con enmascaramiento dinámico de SQL Server (MASKED WITH, 2016+) y una
credencial sin UNMASK sobre la columna o la base, el motor devuelve la
máscara y no el dato. La completitud de esa columna se publica —los no nulos
se cuentan con una expresión que el motor no enmascara—; distintos, moda,
extremos, media, mediana y desvío quedan no_disponible con el motivo, la
columna no entra en perfil_muestra (una fila alcance_distinto lo dice) y
resumen_tabla$cobertura la declara. Con UNMASK se perfila entera.
Si la lectura de la muestra falla, la columna que la hace fallar se busca
por descarte leyendo las MISMAS filas que la muestra con menos columnas —un
fallo que está en una fila que no es la primera, como el CAST de un texto
sucio en una vista, también se encuentra—; la muestra sale con las demás y
una fila alcance_distinto nombra la culpable.
El nombre de la tabla se normaliza antes de la primera llamada a la conexión,
también la que comprueba que existe: un nombre marcado latin1 —el que da
read.csv(encoding = "latin1")— se convierte a UTF-8 y se lee, y uno con
bytes que no son UTF-8 válido y no declaran su codificación —read.csv()
sin fileEncoding sobre un archivo latin1— no se adivina: con odbc da
lupa_error_codificacion_dbi antes de tocar la conexión, que antes quedaba
rota. Una columna de orden_muestra que la sesión no puede escribir da
también lupa_error_codificacion_dbi, con ese remedio: no la rechazó el
motor, no se le mandó.
Los valores de texto tienen el mismo origen. Bajo un locale que no es
UTF-8, FreeTDS entrega las columnas de texto no Unicode (varchar, char,
text) en latin1 y odbc las marca UTF-8 sin convertirlas; las nvarchar
llegan en UTF-8. Un valor así se lee como latin1 —lo que es—, y no como texto
UTF-8 roto: no se publica el hallazgo de bytes que no forman UTF-8 válido, que
sería del controlador y no del dato, y los valores entran en los análisis de
texto y en las comparaciones de relaciones_coleccion(). Lo que no se puede
recuperar es un carácter que no está en latin1 —el signo del euro—: el
controlador lo entrega como ?. Por eso cada columna de texto no Unicode
leída bajo una sesión así queda en resumen_tabla$cobertura con estado
degradado, el motivo y el remedio —correr con un locale UTF-8—, salvo que
el controlador haya entregado en ella texto UTF-8 no ASCII (está configurado
en UTF-8).
Bajo una sesión UTF-8, la columna que el controlador entregue en latin1 —un
ClientCharset configurado así— se lee igual y se declara igual.
Los stop() de esta vía llevan clase de condición propia —todas heredan de
lupa_error_dbi— para que se puedan rescatar con tryCatch():
lupa_error_conexion_dbi, lupa_error_tabla_dbi, lupa_error_campos_dbi,
lupa_error_conteo_dbi, lupa_error_esquema_dbi,
lupa_error_codificacion_dbi y lupa_error_argumento_dbi.
La vía se puede correr dentro de una transacción abierta por el usuario —es
lo que recomienda el motivo de consistencia para una lectura consistente—,
y la transacción sigue como estaba. En PostgreSQL un error cualquiera aborta
la transacción, y la vía manda consultas que pueden fallar a propósito —las
sondas de dialecto— o por el dato —una métrica que el motor no calcula sobre
un tipo, una vista rota—: con una transacción abierta, cada sentencia de la
vía va entre un SAVEPOINT propio y su RELEASE, y si falla se vuelve con
ROLLBACK TO SAVEPOINT. La transacción se reconoce por RPostgres
(dbBegin(), dbWithTransaction()) o por el motor, para un BEGIN mandado
a mano. El paquete nunca manda BEGIN, COMMIT ni ROLLBACK; fuera de una
transacción no agrega ninguna sentencia de ese tipo. Lo mismo vale para
plan_perfilado_dbi(), perfilar_coleccion() y relaciones_coleccion().
SQL Server, MySQL, MariaDB y SQLite no abortan la transacción por el error de
una consulta. duckdb sí la aborta por un error al ejecutar —no por uno al
preparar, como el de una sonda de dialecto— y no tiene SAVEPOINT: dentro
de una transacción de duckdb, un objeto que falla al leerse —una vista que
convierte un texto sucio— la deja abortada, y hay que cerrarla con
ROLLBACK.
universo = "muestra_motor" sondea las formas declaradas por el adaptador y
usa TABLESAMPLE cuando el motor lo acepta, o una función pseudoaleatoria con
el límite del dialecto. Si ninguna forma es compatible, las métricas SQL
quedan en no_disponible: no se sustituyen por resultados de la tabla completa.
Cada registro publica alcance, universo, tamano_muestra, fraccion,
metodo y error_esperado. En resumen_tabla$meta$muestreo,
tamano_muestra conserva el nombre historico y declara el tamano efectivo
solicitado a la consulta; filas_solicitadas y filas_pedidas declaran el
pedido original y filas_obtenidas las filas que devolvio el spool. La
materializacion publica muestra_id, snapshot_id, orden_id, n_filas,
bytes, checksum, backend, version y presupuesto en
meta$materializacion; las pasadas publican el mismo muestra_id.
El contrato de perfil_muestra es campo por campo: meta$filas_analizadas
es la cantidad efectivamente entregada a los diagnósticos; hallazgos solo
contiene familias disponibles; y cobertura_diagnosticos tiene una fila por
familia no evaluada. meta$origen_dbi$muestreo conserva filas solicitadas,
entregadas, reproducibilidad, muestra_id, snapshot_id y checksum. El
método de impresión remite a esa cobertura cuando el campo no está
disponible; no reemplaza la ausencia con NULL silencioso. Cuando el orden
de la muestra del motor no se demostró estable, cada traza de hallazgos se
publica no_disponible con alcance = "orden_muestra_no_estable", sin
índices ni claves y con su total; los patrones raros conservan su cantidad y
retiran sus nombres.
Bajo bloque_filas, perfil_muestra sigue siendo la muestra diagnóstica
acotada por muestra, max_celdas_muestra y max_bytes_muestra; no es la
cobertura completa. Por eso perfil_muestra$general$filas es el tamaño de
esa muestra y la cobertura se lee en meta$bloques$filas_vistas.
En muestra_motor, todas las metricas SQL salvo n describen la relacion
muestreada; n es el total de la tabla completa y esta marcado con
alcance = "tabla_completa", metodo = "conteo_universo" y
error_esperado = "no_aplica". Los conteos n_validos, n_faltantes,
prop_faltantes, n_distintos, tasa_distintos, n_ceros,
n_negativos y frecuencia_moda son observaciones de esa muestra, no
extrapolaciones al universo. Sus motivos declaran la escala local y el
denominador n_total_consulta cuando corresponde.
TABLESAMPLE SYSTEM es la fuente preferida cuando el adaptador la declara y
la sonda la acepta; publica metodo_muestreo = "tablesample_system",
sesgo_muestreo = "por_bloques" y el motivo estable
sesgo_muestreo:tablesample_system_por_bloques. Si se usa el fallback
random_limit con NEWID(), la metadata publica
metodo_muestreo = "random_limit", funcion_muestreo = "newid",
sesgo_muestreo = "por_fila" y
sesgo_muestreo:random_limit_newid_por_fila. Ese fallback se acepta solo
si la politica de costo conserva n_total, la proyeccion, la tasa y sus
tres umbrales; si no, la mediana queda no_disponible con
capacidad_no_aceptada:newid_costo_excede_presupuesto aunque se silencien
los avisos.
Si la consulta de la muestra devuelve cero filas, no hay base para medir las
metricas de alcance muestra: se publican con valor NA, estado
no_disponible y el motivo estable
muestra_vacia:random_limit_sin_filas (o el metodo efectivo equivalente).
Esto no permite concluir que
la columna este vacia, por lo que no se publica cero ni se dispara la
cascada sin_valores. Una muestra no vacia sin valores validos usa
muestra_inestable:sin_valores_validos; una capacidad no aceptada usa
capacidad_no_aceptada:.... n conserva el conteo de la tabla completa.
En una muestra, error_esperado vale no_estimado para metricas cuyo error
podria calcularse bajo un plan probabilistico pero no se calculo,
no_estimable para la moda, la mediana y la cardinalidad observada, que no
tienen una cota simple sin supuestos o un estimador declarado, y no_aplica
cuando no hubo muestreo efectivo. El motivo de cada registro explica la
distincion. metodo, tamano_muestra y fraccion conservan las condiciones
de la corrida; no se publica una cota numerica sin una formula justificada.
Los distintos de una muestra se publican como cardinalidad de la muestra,
no como cardinalidad del universo. estrategia_distintos es explicita y
vale "exacta" por omision: calcula COUNT(DISTINCT) sobre las filas de la
corrida. "aproximada_motor" sondea una funcion nativa y, si no hay una
capacidad aceptada, deja la metrica no_disponible; nunca ejecuta el conteo
exacto como repliegue. "catalogo" lee pg_stats.n_distinct y publica
estimado_catalogo sólo cuando universo = "tabla_completa". Con
universo = "muestra_motor" queda no_disponible, porque el catálogo
describe la relación entera y la corrida mide un subconjunto; no se inventa
una equivalencia entre universos.
"omitida" no emite la
consulta. Cada resultado y el atributo meta$estrategia_distintos separan
estrategia_solicitada, estrategia_resuelta y estado.
Las comparaciones que tienen una cota dura usan solo valores del mismo grupo
de consistencia. En esta version, el grupo queda probado por el
consulta_id que ya se registra en resumen_tabla$sql: dos metricas con el
mismo identificador salieron de la misma sentencia. La consulta exacta de
distintos trae el conteo de no nulos AS n_validos_guard junto a
COUNT(DISTINCT columna). Si una capacidad aproximada sólo construye una
consulta completa y no puede traer ese guardian, la cota no se comprueba y
el motivo lo declara; no se atribuye un valor imposible al motor.
La consulta de la moda intenta traer SUM(COUNT(*)) OVER () AS n_validos_guard junto a su frecuencia. La forma se sondea antes de usarla;
si el motor la rechaza, se conserva la consulta anterior y
meta$moda_guardian publica el repliegue. Cuando la sonda pasa, la cota
frecuencia_moda <= n_validos se comprueba dentro de la misma sentencia y
el motivo de la métrica lo declara.
meta$snapshot queda en FALSE, siguiendo la declaracion de colecciones: no
hubo lectura instantanea del motor. snapshot_id identifica positivamente la
materializacion cliente, pero solo se publica evidencia positiva de snapshot
transaccional cuando el adaptador la demuestra. La cobertura agrega una entrada concreta solo si
n_validos y n_distintos son exactos, incoherentes y provienen de grupos
distintos; su motivo conserva ambas sentencias.
La mediana muestral que no tiene una forma consolidada o escalar usa una
sola CTE con COUNT(*) OVER (), ROW_NUMBER() OVER y el promedio de las
posiciones centrales. La sonda barata repite esa construccion sobre
VALUES (1.0), (2.0), (3.0), (4.0), exige 2.5 y un control negativo que
falle; no se sondea la tabla de produccion. Si esa capacidad falla, la
mediana se publica como no_disponible con
capacidad_no_aceptada:sonda_mediana_cte_ventana, nunca como
dos_consultas bajo muestra_motor.
Las cotas de error no documentadas de una aproximacion nativa quedan como
"desconocido". Una aproximacion solo se consolida cuando entrega una
expresion que se puede incrustar en el SELECT; si solo construye una
consulta completa, se emite por separado. Una consulta no emitida o sin un
valor utilizable queda no_disponible.
Acotar filas no es SQL estándar. En vez de suponer LIMIT, la función
sondea el motor con una consulta de cero filas y elige la primera capacidad
que acepte: LIMIT n [OFFSET k], TOP (n) y OFFSET … FETCH NEXT,
FETCH FIRST … ROWS ONLY, o ROWNUM. Si ninguna sirve queda la vía
portable, que acota en el cliente con dbSendQuery() y dbFetch(n); en ese
caso la mediana exacta se declara no disponible en vez de traer media tabla
a memoria. El alias de subconsulta se escribe con AS o sin él según el
motor, y los alias de columna van comillados y se comparan sin distinguir
caja, porque hay motores que los pliegan a mayúsculas.
Para los motores del dialecto limit, la mediana exacta usa una sola
sentencia: el COUNT queda como subconsulta escalar de la consulta que
ordena y recorta. La forma se sondea antes de usarla; en SQLite y
PostgreSQL se usan % y / con division entera. Bajo
muestra_motor, los dialectos sin una forma escalar ni consolidada usan la
CTE de ventanas descrita arriba y no degradan a dos_consultas. En
tabla_completa se conserva el camino previo, incluido dos_consultas si
es el que resuelve ese dialecto; PERCENTILE_CONT no cambia.
La mediana consolidada tambien se sondea antes de usarla; en SQL Server
requiere nivel de compatibilidad >= 110. Se sondea; estos son los motivos
conocidos de que no se active: la funcion no esta disponible o el motor
rechaza la sonda. En ese caso el mensaje del motor queda en
meta$mediana_consolidada$motivo —sin sus literales con la protección
activa, entero sin ella, como todo mensaje del motor: ver «Datos
personales»—, y se conserva la mediana por columna.
El resumen SQL describe lo que el motor tiene delante, y en estos casos eso no es lo mismo que la tabla en R:
Fechas en SQLite. SQLite no tiene tipo fecha: RSQLite guarda un Date
como número de días desde 1970 y un POSIXct como segundos, y sin más
información el motor los describe como números —minimo = 17956—. Con
DBI::dbConnect(RSQLite::SQLite(), ..., extended_types = TRUE) la columna
se declara temporal y perfilar_dbi() no le aplica agregados
cuantitativos, y lo dice.
NaN en duckdb. duckdb conserva NaN como un valor: lo cuenta como
presente y distinto, y responde MAX y AVG con NaN. En R, NaN cuenta
como ausente. Los conteos pueden diferir por eso, y una métrica que el motor
respondió con NaN no se publica: queda no_disponible con su motivo.
Infinitos. SQLite, PostgreSQL y duckdb admiten Inf y -Inf en una
columna de punto flotante, y el motor los cuenta en MIN, MAX, AVG y
el orden de la mediana. Los estadísticos de magnitud se calculan sólo con
valores finitos, como en perfilar() y con bloque_filas: cuando el motor
responde un infinito, minimo, maximo, media, mediana, desvio,
n_ceros y n_negativos se vuelven a pedir sobre una subconsulta que deja
afuera los infinitos, y su sql lo muestra. Donde la consulta no sabe
escribir el infinito, o sobre una muestra del motor, esas métricas quedan
no_disponible con ese motivo. Un infinito es un número: no se informa
como un valor que no se pudo leer como número.
money y real en PostgreSQL. RPostgres lee 0 en toda celda
money, sin avisar, y un real por el texto con que lo escribe el
servidor, mientras el motor opera sobre el binario guardado
—21.299999237060547—. Las dos columnas se leen convertidas en el motor:
money como CAST(x AS NUMERIC) y real por la forma decimal más corta
que vuelve al valor guardado —21.3, la cifra que lee R desde PostgreSQL
12—, que se calcula en el motor redondeando a 6, 7, 8 y 9 cifras. No se
toma del texto de la sesión: antes de PostgreSQL 12, o con
extra_float_digits de 0 o menos en cualquier versión, ese texto tiene
seis cifras y 1234501 se escribe 1.2345e+06 —200 valores distintos se
leían como 21, con un mínimo que no estaba en la columna—. Así la lectura
da el valor guardado en cualquier versión y con cualquier
extra_float_digits desde -6, sin tocar el estado de la sesión; en un
empate de la última cifra puede elegir la otra forma corta, que vuelve al
mismo real. Por debajo de -6 el servidor escribe todo doble con menos de
nueve cifras, y ninguna lectura —tampoco la de un double precision—
llega entera a R. La conversión vale para toda lectura —la
muestra, bloque_filas, muestra_motor— y el resumen SQL calcula sus
agregados sobre esa misma lectura, así que las vías publican la misma
cifra. Las columnas convertidas y su expresión quedan en
resumen_tabla$meta$lectura_convertida. Se reconocen por el tipo que
publica DBI::dbColumnInfo() de RPostgres; con otro controlador no se
convierte nada.
La media en coma flotante. Cada lado suma a su modo; sobre una columna
que mezcla magnitudes muy distintas —1e20 junto a 1— las medias pueden
diferir entre motores y con la de R, y ninguna es la exacta. La
corroboración cruzada lo declara cuando compara las dos. La media y el
desvío se le piden al motor como doble donde su aritmética decimal los
recortaría: en SQL Server AVG(CAST(x AS FLOAT)), porque AVG de un
numeric devuelve seis decimales, y en PostgreSQL
STDDEV_SAMP(CAST(x AS DOUBLE PRECISION)), porque sobre magnitudes
grandes el desvío de un numeric sale con cuatro; en MySQL y MariaDB
AVG(x * 1e0), porque AVG de un DECIMAL devuelve cinco decimales. En
los demás motores, x * 1.0.
El desvío de una sola pasada. STDEV de SQL Server —y, con menos
pérdida, el desvío nativo de SQLite, duckdb, MariaDB y MySQL— acumula en
una sola pasada, y cuando la media es muy grande frente al ancho de la
columna pierde las cifras del desvío: sobre marcas de tiempo en
milisegundos de una ventana de un segundo, SQL Server respondía 0. El
desvío no cambia si a cada valor se le resta una constante, así que se le
pide al motor sobre los valores centrados en la media ya calculada: siempre
en la consulta por columna, y en la consulta compartida —donde la media sale
de la misma sentencia— se vuelve a pedir centrado cuando la media supera
diez veces el desvío, o cuando respondió 0 sobre una columna con valores
distintos, con una consulta más que el plan no anticipa porque
depende de los datos. Su fila de sql lo dice. La resta se hace en un tipo
que no pierde cifras: en SQL Server y duckdb una columna real (FLOAT en
duckdb) restaba en 24 bits —en T-SQL real - decimal es real— y el
desvío centrado se apartaba 1,4e-8 del de los valores guardados, así que
se convierte a doble antes de restar; en SQL Server el centro de una
columna de coma flotante va como literal float, que no tiene el tope de 38
cifras de un decimal. Una columna decimal o entera resta en su tipo, que es
exacto. Con bloque_filas los
momentos se acumulan centrados. Bajo muestra_motor no se vuelve a pedir,
porque otra consulta vería otras filas; ahí, como red en todas las vías, un
desvío 0 sobre una columna con dos valores distintos —un mínimo menor que
el máximo— es imposible y queda no_disponible con su motivo.
El cotejamiento. El resumen SQL agrupa y ordena con el cotejamiento
de la columna: sobre una COLLATE NOCASE de SQLite o una colación ICU no
determinista de PostgreSQL, "Montevideo" y "montevideo" son un valor, y
n_distintos, la moda y su frecuencia salen de esa cuenta. Con
bloque_filas las filas se leen y se comparan en R, byte a byte, como el
perfil de muestra. Cada resumen_tabla lo dice en
meta$criterio_comparacion y meta$criterio_moda. La corroboración no
llama divergencia a la diferencia que ese criterio explica —menos
distintos en el resumen SQL, con la moda y la frecuencia que de ahí se
siguen, o una moda empatada que cada lado desempata con su orden—: la deja
en una fila no_corroborada con ese motivo.
Una columna que mezcla tipos de almacenamiento. SQLite admite texto en
una columna de afinidad numérica, y RSQLite la lee como número: convierte
ese texto en 0 y lo avisa ("mixed type"). El resumen SQL ve los valores
guardados —la moda puede ser el texto, y las métricas de magnitud quedan
no_disponible porque el motor devolvió algo que no se lee como número—,
y perfil_muestra ve lo que entregó el controlador: minimo = 0, una moda
0 que nadie escribió. perfilar_dbi() no relee la columna como texto:
cuenta con typeof() los valores guardados como texto o blob y declara
cada divergencia con esa causa medida en resumen_tabla$cobertura,
y dice que las cifras de magnitud de perfil_muestra para esa columna no
describen lo guardado. Para perfilar los valores guardados, leer la columna
como texto (CAST(columna AS TEXT) en una vista) o limpiarla en el origen.
Limitación conocida: un texto que R sí lee como número —'0x10',
hexadecimal, que as.numeric() lee 16— pasa la sonda de magnitud como un
número, y el resumen SQL publica maximo = 16 sobre una columna NUMERIC
cuyo máximo numérico es otro; las divergencias de la corroboración son la
señal.
Lo que la corroboración compara. Sólo las métricas que los dos bloques
calculan con la misma semántica. No compara lo que una declaración o una
guarda hacen distinto a propósito, porque esa diferencia no es del motor,
del controlador ni de un snapshot: la media, la mediana y el desvio
de una columna de columnas_codigo, que no se calculan; n_faltantes de
una columna de columnas_opcionales o de aplicabilidad, que la muestra
cuenta sobre su universo —y todas las métricas de la columna con
aplicabilidad si la muestra dejó afuera valores presentes—; las métricas
de magnitud de una columna de la que la muestra excluyó los
sentinelas_numericos declarados; y las de un entero de 64 bits desde
2^53, que el resumen no publica. Tampoco lo que un bloque no calculó: una
métrica que resumen_tabla$sql declara no_disponible —el motor rechazó
la consulta sobre el tipo (json, xml o point de PostgreSQL; text,
ntext, image o geography de SQL Server), una guarda la retuvo—, la
moda que la muestra no pudo contar —una columna de blobs— y las métricas
de una columna compuesta, que la muestra no analiza: su motivo ya está
publicado, y una relectura daría lo mismo. Un NaN o un infinito sí se
compara, y un valor guardado como texto en una columna numérica también:
es la diferencia del motor que la corroboración existe para declarar. Lo que
no se comparó queda en resumen_tabla$meta$corroboracion_bloques$no_comparadas,
una fila por columna y métrica con su causa. La tolerancia está en
meta$corroboracion_bloques$tolerancia: 1e-10 absoluta más 64 épsilon
de máquina relativos para los valores leídos —minimo, maximo,
mediana, la moda—, y 1e-10 relativa para media y desvio, que cada
lado acumula con su algoritmo y en su orden. Medido en SQL Server, la
diferencia de la acumulación llega a 2e-13; una divergencia real es de otro
orden y sigue saliendo. Limitación conocida: la tolerancia absoluta cubre
cualquier diferencia en una columna cuyos valores son menores que 1e-10
en valor absoluto —p-valores, concentraciones en unidades del sistema
internacional—, así que ahí la corroboración no distingue dos lecturas.
Los agregados de una tabla ancha se emiten por lotes; muestra acota lo que
se trae a R, no el trabajo del motor. bloque_muestra decide si se trae esa
muestra; universo, metricas, tamano_lote_planos, tamano_lote_distintos y
max_consultas acotan el trabajo SQL. plan_perfilado_dbi() dice cuántas
consultas se van a emitir antes de emitirlas. El orden de degradación es
agregados planos, total del universo cuando hace falta, distintos, moda y
mediana. Los
agregados planos sobre la misma tabla y filtro —el conteo de no nulos,
mínimos, máximos, medias, ceros, negativos y desvío— comparten una consulta
por lote y cada consulta que trae n_validos lleva además
COUNT(*) AS n_total_consulta en la misma sentencia. La completitud usa ese
denominador local, no el total de otro lote. Los no nulos se cuentan en
todos los motores con COUNT(CASE WHEN col IS NULL THEN NULL ELSE 1 END) y no
con COUNT(col): SQL Server rechaza COUNT() sobre text, ntext e
image, y con enmascaramiento dinámico devuelve COUNT(col) enmascarado
—0—; la pregunta por el nulo la contesta sobre cualquier tipo y sin máscara.
La fusión conserva la medición, no una identidad bit a bit entre
agrupamientos: la media y el desvío pueden diferir en el último bit según
cómo se agrupen las sumas, porque la suma en punto flotante no es asociativa.
El total del universo se conserva por separado cuando el perfil se calcula
sobre una muestra. Si el lote completo es rechazado, sus mitades se sondean
por bisección: los grupos
aceptados se reutilizan como mediciones y las columnas culpables se reintentan
por métrica, con su denominador local. Las fuentes TABLESAMPLE que necesitan
el total del universo para escribir un porcentaje lo cuentan antes.
COUNT(DISTINCT ...) queda en una clase separada y usa su propio tamaño de
lote, conservador por omisión porque una cardinalidad puede derramar mucho
más que veinte agregados planos; la consulta exacta trae su
n_validos_guard compañero.
Cuando se pide mediana o desvio sin pedir basicos, cada columna que el
esquema expone como numérica paga además una sonda propia de magnitud. Usa
MIN para comprobar que el motor no está coaccionando texto a cero; la fila
sonda_magnitud queda declarada en resumen_tabla$sql, con su estado y la
etapa sonda_magnitud. Si la sonda no entra en max_consultas, las métricas
dependientes quedan no_disponible con el motivo del presupuesto. Cuando
incluir_valores = FALSE, el motivo no publica el valor que devolvió el
motor.
La proyección temporal no usa esos agregados planos: si hay más de un lote y
instrumentar = TRUE, se mide el primer lote de distintos y, después de
ejecutarlo, se multiplica su mediana por la cantidad total de lotes. El aviso
llega antes del segundo lote, en la unidad que se va a evitar; con un solo
lote no hay nada que proyectar. Si la duración no se pudo medir, el resultado
declara la proyección como no disponible.
La moda tiene otro canal: después de cada moda medida se obtiene una tasa en
ms por distinto y se usa para proyectar las modas pendientes. La cardinalidad
se toma del agregado de la corrida, de una clave garantizada o de la
estimación de catálogo que esté disponible; si falta, meta$costo_moda lo
declara y no inventa un número. El aviso llega antes de la siguiente moda.
La mediana se proyecta en ms por fila. La primera mediana medida en esta
corrida sirve para proyectar las restantes y el aviso precede a ese trabajo.
Si no existe una primera medición local para una mediana total, usa la
referencia declarada de otra corrida de 68 ms por millón de filas.
Si la consulta inicial que obtuvo las filas fue medida y resulta una cota
mayor, se publica también esa cota de lectura —no como medición de mediana—
para no subestimar una tabla grande recién cargada.
Las dos proyecciones quedan separadas en meta$costo_moda y
meta$costo_mediana; apagar el aviso no apaga su medición ni su metadata.
Antes de la primera consulta exacta se estima, cuando PostgreSQL expone
pg_stats, el tamaño de los hashes con n_distinct, avg_width y
pg_class.reltuples; SHOW work_mem y, desde PostgreSQL 13,
SHOW hash_mem_multiplier dan el límite efectivo. meta$estimacion_derrame
y attr(meta$plan, "estimacion_derrame") conservan el diagnóstico, siempre
rotulado como estimación y nunca como derrame medido. Si supera el límite se
avisa antes de pagar COUNT(DISTINCT) y se recomienda subir work_mem en la
sesión; el paquete no lo modifica. Una estadística ausente, un permiso
insuficiente o una versión sin el parámetro dejan la parte correspondiente
como no disponible. n_distinct es una estimación de muestra y puede quedar
corta; si luego pg_stat_statements mide un derrame, esa medición manda.
La misma corrida publica meta$estimacion_derrame_moda y
meta$estimacion_derrame_mediana. La moda deriva metodo del plan exacto
mediante EXPLAIN (FORMAT JSON, COSTS OFF) sin ANALYZE; la mediana usa
siempre un sort contra work_mem, con pisos de 32 bytes para tipos fijos y
42 para numeric. El plan usa n_validos de catálogo. En la meta y en el
aviso, cuando se pidió la familia validos, ese denominador se reemplaza por
el medido en los agregados planos y se conserva también el valor de catálogo;
si no se midió, el motivo declara que se retuvo el catálogo. Por arbitraje
H5, este alcance K2 gobierna solo el denominador: si la estimación publicada
marca supera_memoria = TRUE, el aviso se emite también con catálogo y
declara su fuente. La consolidada
decide por el máximo de columna y publica la suma de tapes sólo como
estado_io_total_bytes informativo.
Los avisos de esta vía son deliberadamente distintos de los de perfilar():
perfilar_dbi() los emite también en guiones no interactivos porque el costo
relevante ocurre en el servidor y puede consumir decenas de segundos antes
de que el llamador pueda hacer algo. En cambio, el aviso de tabla ancha de
perfilar() estima trabajo local sobre una tabla ya en R y queda limitado a
sesiones interactivas para no convertir la salida de un guion en ruido.
Cada aviso DBI tiene su propio interruptor y umbral porque sus unidades no
son comparables —segundos frente a bytes— y silenciar uno no debe ocultar el
otro. Apagar un aviso no apaga ninguna medición: meta$costo_distintos,
meta$costo_moda, meta$costo_mediana, meta$derrame y
meta$estimacion_derrame se publican igual.
Lo que no entra en el presupuesto queda en no_disponible con su motivo,
nunca en cero. meta$tamano_lote_funciono conserva el mayor lote aceptado
durante esa corrida; no se guarda estado global asociado a la conexión.
resumen_tabla$sql conserva una fila por métrica y agrega la duración de la
consulta que la respalda, las filas devueltas y los bytes que ese resultado
ocupa en R. consulta_id identifica el intento dentro de la corrida y
duracion_ms es la duración de la consulta, repetida en cada fila que esa
consulta produjo; no es una duración por métrica. La columna nivel marca
qué filas se pueden sumar: la primera fila de cada consulta_id queda en
nivel = 1 y sus repeticiones en nivel = 2. Por eso una suma segura usa
sólo duracion_ms[nivel == 1] (con na.rm = TRUE si corresponde), no la
columna completa.
id_consulta identifica la consulta de datos que produjo la medición: dos
métricas con el mismo identificador vieron exactamente las mismas filas y se
pueden comparar directamente. NA declara que esa garantía no se puede
hacer; en particular, las métricas por columna —moda, frecuencia de la moda
y mediana— no comparten filas con otras métricas. etapa permite agruparlo
(conteos, moda, basicos, mediana,
desvio, lectura_muestra y las sondas). Las métricas no solicitadas o que
no emitieron consulta conservan esos campos y los dejan en NA; en
particular, NA no significa cero.
resumen_tabla$sql$memoria_trabajo agrega el eje del estado de trabajo del
motor: se deriva en orden como NA para una fila sin medición, acotado
para una muestra con fraccion < 1 y, en los demás alcances efectivos,
según el método resuelto del registro; una muestra saturada (fraccion = 1)
cae a este último caso porque su tope es vacuo y se clasifica como tabla
completa. Sus únicos valores son
"creciente", "acotado" y NA.
En PostgreSQL, con instrumentar = TRUE, se toma una foto de
pg_stat_statements antes y después de la ventana que cubre los agregados
exactos instrumentables de distintos, moda y mediana. Las firmas se filtran
por texto y se normalizan en el paquete: cada literal numérico o de cadena
se reemplaza por su parámetro posicional y los identificadores entre comillas
dobles se conservan. Sólo se publica un derrame cuando una consulta coincide
y su contador aumentó en al menos una llamada dentro de la ventana. En ese caso,
resumen_tabla$sql agrega derrame,
bloques_temporales_leidos, bloques_temporales_escritos y
fuente_derrame, además de llamadas_en_ventana; resumen_tabla$meta$derrame
conserva el resumen y la fuente. Cuando el contador aumentó más de una vez,
el motivo declara que los bloques son un agregado de ese texto exacto y
pueden incluir otra sesión o llamada concurrente. Si la extensión no está
disponible o la instrumentación está apagada, el estado queda
no_disponible o no_medido: el paquete no deduce un derrame del tiempo ni
modifica work_mem.
resumen_tabla$meta$estimacion_derrame es un diagnóstico distinto: conserva
la estimación de memoria y siempre la rotula como no medida. Puede quedar
parcial o no disponible por permisos, falta de ANALYZE, particiones sin
estadísticas o un motor que no sea PostgreSQL. Si ambos diagnósticos existen,
meta$derrame es la evidencia posterior y prevalece sobre la estimación;
una estimación que no superó el límite no contradice un derrame medido.
meta$estimacion_derrame_moda y meta$estimacion_derrame_mediana siguen la
misma regla, con el método del plan de la moda y el sort de la mediana;
n_validos_medido sólo aparece con valor cuando la familia validos fue
solicitada y sus agregados planos lo pudieron medir.
resumen_tabla$tiempos reúne las etapas grandes del cliente en las mismas
unidades (duracion_ms): lectura_muestra, perfilado_muestra,
perfilado_columnas, y los análisis opcionales cuando se solicitan. Una
etapa apagada queda con estado no_solicitado; si la instrumentación se
apaga queda con estado no_medido y duración NA. La columna nivel dice cuáles se
pueden sumar: las de nivel = 1 son disjuntas entre sí, y las de nivel mayor
están contenidas en alguna de ellas. perfilado_muestra es inclusivo
-contiene el perfilado por columna, las dependencias y los casi-duplicados-,
así que sumar la columna entera da más que la corrida.
proteger_datos_personales viaja en ... hacia perfilar() y vale TRUE
por omisión. La protección alcanza a los dos bloques: en resumen_tabla se
reemplaza la moda de las columnas clasificadas como personales, se ocultan
sus estadísticos de orden y sus momentos, se sanean los datos de conexión y
el SQL guardado no contiene ningún valor derivado de los datos. La
clasificación se toma del perfil de la muestra; si la muestra no se pudo
leer, la protección se aplica a todas las columnas y meta lo declara.
Lo que se oculta no queda calculado: su fila de resumen_tabla$sql dice
omitido_por_privacidad, con un motivo que distingue la columna clasificada
como personal de la que se cerró porque, sin muestra, no se pudo clasificar.
Una columna que no se protege publica sus estadísticos aunque comparta el
tipo personal con una protegida, como en perfilar(): un importe entero de
siete cifras o un número de factura clasificado documento_identidad
débil. Hasta la ronda 33 se tapaban sus cinco estadísticos con el motivo
de una columna protegida, mientras perfil_muestra los publicaba; una
copia del documento la tapa el piso por valor, que corre con la tabla
entera, y la que lo copia sin su verificador se clasifica como columna
personal con la regla de perfilar(), sobre la muestra. Por la vía por
bloques eso incluye meta$familias, que guarda cada valor distinto de
toda columna no protegida: una columna debil —las cédulas sin su
verificador de OTRAS personas, como las de un garante— publica ahí cada
uno de sus valores, donde perfilar() publica sus dos extremos.
Limitación conocida (baja): como la clasificación es sobre la muestra, con
una muestra que ve menos de tres valores distintos de la copia —chica y
ordenada sobre una tabla agrupada por persona— la copia no se clasifica, y
el resumen de la tabla entera publica sus extremos, que pueden ser
documentos de personas que la muestra no vio; por bloques, cada valor
distinto en meta$familias. Una muestra mayor o aleatoria lo evita, y
también declararla en columnas_personales.
El desvío se oculta como la media, como en perfilar(); también en
resumen_tabla$meta$familias, donde la vía por bloques (bloque_filas)
guarda lo que devolvió cada acumulador: ahí se tapan los extremos, la media
y el desvío de las columnas protegidas, y cada valor distinto. Y por la vía
por bloques corre el mismo piso por valor que sin ella —con los valores de
la muestra y, si la muestra no cubre la tabla, con los de la tabla entera—,
sobre todas las columnas: una cédula copiada en una columna de códigos o
un nombre citado en un texto libre se tapan en resumen_tabla$columnas, en
la corroboración y en meta$familias igual que sin bloque_filas; la misma
tabla publica lo mismo por las dos vías. Lo que el piso tapa en la tabla de
columnas —un extremo que es un documento, y la media y el desvío que lo
reconstruyen— se tapa también en meta$familias.
Si la muestra se recorta, la clasificación personal sólo cubre esas filas:
las cifras de valor del resumen SQL completo quedan marcadas en
resumen_tabla$cobertura y, con la protección activa, se ocultan hasta que
la clasificación cubra el mismo alcance. La corroboración no repite esas
cifras dudosas en su anotación. Y cuando la muestra no cubre la tabla, se
traen los valores distintos de las columnas protegidas de la tabla entera
—de texto o numéricas, una consulta por columna— y se protege con ellos lo
que se publica, con la misma regla que perfilar() en memoria: tildes, caja,
separadores, el documento parcial, los campos numéricos y la evidencia de los
hallazgos. Un valor de una persona que sólo está fuera de la muestra se tapa
igual. El relleno que sale del piso en memoria —el dígito repetido cinco
veces o más en una columna que sin él es casi una clave— se decide sobre la
tabla entera, no sobre la muestra: en la muestra de una tabla de movimientos
los clientes que se repiten casi no se repiten, y la cédula válida de un
cliente con seiscientos movimientos salía como moda de otra columna. Con
la tabla leída completa decide la muestra, que es la tabla; si no, la
confirman los conteos exactos de la tabla —n_distintos y n_validos
calculados—, y sin ellos el relleno sigue en el piso. Con
universo = "muestra_motor" no hay tabla entera que lo confirme —los
conteos son los de la muestra del motor, y con muestra = Inf la muestra en
R es esa misma relación—: el relleno sigue en el piso. Y sale del piso sólo
en su columna, como en memoria: si otra columna protegida tiene el mismo
valor —el 1.111.111-1 de relleno de cedula es la cédula de un cónyuge—,
ese valor se sigue tapando. Y la protección es
por tabla: un valor protegido de otra tabla escrito en ésta no se
conoce; perfilar_coleccion() los cruza. Se traen hasta dos millones de valores; si hay más, o si la consulta
no se puede hacer, se tapan todos los valores de celda de las columnas no
personales —moda, extremos, ejemplos y, por la vía por bloques, cada valor
distinto y los extremos de meta$familias— y la evidencia de sus hallazgos, y
resumen_tabla$meta$proteccion_personal$fuera_de_muestra dice por qué. Salvo
un valor igual al nombre de la tabla o de su esquema, que es estructura: se
publica en meta, en cada SQL guardada y en print(), y un valor de celda
igual a él no se tapa —tapaba el nombre de todo el objeto—; lo mismo cuando
sin muestra se cierran todas las columnas. Limitación conocida (baja): un
valor igual al nombre de una columna sí se tapa, porque un nombre de
columna puede ser el de una persona —una planilla con una columna por
docente—, y tapa también ese nombre en la descripción de las sondas de
dialecto (meta$dialecto$sondas) y, con bloque_filas, en la de la fuente
por bloques (meta$fuente_bloques, meta$plan); en columnas$columna y
en la SQL guardada queda. En
SQLite los valores se leen como texto, porque una columna de afinidad
numérica guarda texto y el controlador lo convertía en número; también sin
muestreo, cuando la muestra es la tabla entera: ahí se releen como texto, con
una consulta, las columnas protegidas que el controlador no leyó como texto.
Y el motivo
de una cifra que no se pudo leer como número no cita el valor de una columna
protegida. Tampoco el mensaje de un error del motor, que es texto ajeno
y cita la celda que hizo fallar la consulta —invalid input syntax for type bigint: "4.685.467-9", en una vista que castea una cédula escrita con
puntos—: ese valor nunca se leyó, así que el piso no lo conoce y puede ser
de cualquier columna. Con la protección activa, todo mensaje de error que se
publica —en sql, cobertura, meta, en la colección y en los avisos— lleva
sus literales entre comillas —simples, dobles, tipográficas, bajas como las
del polaco, angulares— cambiados por [literal omitido]; también
un identificador entre comillas, salvo el nombre de la tabla o de una de sus
columnas. Y fuera de las comillas el mensaje publica sólo lo permitido:
el vocabulario de los mensajes de los motores, en inglés y en castellano,
las palabras de SQL, los nombres de tipo, los nombres de la tabla y de sus
columnas, los códigos SQLSTATE, los signos y los números de menos de seis
cifras —el piso de la protección: la línea, la posición, un código—;
todo lo demás sale [literal omitido]. Un número se cuenta con sus
separadores: 4.685.467-9 es uno de ocho cifras, y se lee como lo lee el
piso de la protección, con los mismos separadores —un espacio duro o uno
fino, el punto medio, una raya, " - ", dos espacios, un tabulador o un
salto de línea entre los grupos: "099 - 123 - 456" es uno de nueve—. El
mensaje se lee como lo dijo el motor: odbc lo arma con cli, que pone una
viñeta después de cada salto de línea y corta las líneas al ancho de la
consola, y un número partido en dos líneas —"2901\n1234"— quedaba partido
por la viñeta; se toman las líneas que guarda la condición, unidas por el
salto que puso el motor, y así se publican, sin las viñetas. Tampoco llevan
los colores ni los enlaces que cli pone en una consola a color —la de
RStudio—: las secuencias ANSI quedaban guardadas en el motivo, y con
protección pegadas a los marcadores. Lo que cli agrega dentro de una línea
no se distingue del texto del motor y queda: sin color, cli cita entre
comillas el archivo del controlador ('nanodbc/nanodbc.cpp:1802') y con
protección esa cita sale [literal omitido]; con color se publica. Así
se tapa también el
valor que el motor cita sin comillas —duckdb: Type INT64 with value 59899123456 can't be cast, y la cadena que strptime repite en otra
línea, que se tapa entera; SQL Server con la sesión en sueco o neerlandés—
y la cola de un valor que lleva una comilla adentro. Y el mensaje pasa
también por el piso con los valores protegidos que la llamada ya leyó —en
perfilar_coleccion(), los de todas sus tablas—, antes de las dos pasadas,
con el valor entero a la vista, y otra vez al final: un valor escrito sólo
con lo permitido —los apellidos "Campos Cadena", dos palabras del
vocabulario, o un nombre cuyas palabras son columnas de otra tabla— se
tapa entero, y el nombre de una columna que es también uno de esos
valores —la docente "Florencia" de una planilla con una columna por
docente— deja de exceptuarse, con la comparación del piso. En
perfilar_coleccion() cada mensaje se vuelve a leer al final, desde como
llegó, con todo lo que la colección leyó: con una vista perfilada antes que
su tabla base, el piso no conocía todavía el nombre y la segunda pasada
dejaba su resto —"De Los Campos" de "María De Los Campos"—. Lo que queda
abierto: un valor que la llamada no leyó —la celda que hizo fallar la
lectura de la muestra, de una columna que la vista castea— escrito sólo con
lo permitido, o igual al nombre de una columna, se publica, porque el
mensaje no tiene cómo distinguirlo del vocabulario ni de un nombre. Un mensaje en otro
idioma conserva su SQL, sus tipos y sus nombres y se lee peor, que es el
lado cerrado. Quedan una palabra de SQL entre comillas —Incorrect syntax near 'LIMIT' dice que el motor no acepta ese dialecto— y un signo suelto.
El texto que PostgreSQL cita sin comillas al no poder leer un JSON
(CONTEXT: JSON data, line 1: ...) se tapa hasta el fin de la línea. Se
publicaba tal cual, en PostgreSQL, en duckdb y en SQL Server.
Sin protección (proteger_datos_personales = FALSE) el mensaje del motor
se publica entero, en todas esas salidas: el literal que hizo fallar la
lectura y el SQL que la hacía, sólo sin las viñetas, los cortes ni los
colores que agrega cli. El diagnóstico es el cometido del paquete —Conversion failed when converting the nvarchar value 'N/D' to data type int dice que la columna
trae un faltante disfrazado—, y sin protección ya se publican valores de
todas las columnas —modas, ejemplos, patrones—: omitirlo ahí no protegía
nada y quitaba el diagnóstico. Vale igual en perfilar_coleccion(), que pasa
la bandera a cada tabla y la aplica a sus propios motivos.
plan_perfilado_dbi(), coleccion(), estimar_costo_coleccion() y
relaciones_coleccion() no tienen la bandera: llamadas por su cuenta, tapan
el mensaje como con la protección activa.
incluir_valores = FALSE va más lejos: no emite las consultas de moda ni de
mediana y no informa mínimo ni máximo, útil cuando la tabla es un padrón y
la moda de un identificador único es un documento real. Si se pidió desvio,
la sonda de magnitud que ese cálculo necesita puede emitirse, pero su motivo
nunca publica el valor devuelto por el motor.
Traer la tabla entera puede tardar minutos sobre una tabla grande, y una corrida callada no se distingue de una colgada. En una sesión interactiva se muestra una barra que avanza contra las consultas que el plan dice que se van a emitir —un total conocido, no una estimación—, y no aparece cuando la corrida es de menos de una docena de consultas, porque termina antes de que sirva.
options(lupa.progreso = TRUE) la fuerza y FALSE la apaga; fuera de una
sesión interactiva está apagada, para que la salida de un guion no traiga
ruido que despues haya que filtrar. No cambia ningún valor de lo que se mide:
hay una prueba que compara el perfil con la barra y sin ella.
plan_perfilado_dbi(), perfilar()
if (requireNamespace("DBI", quietly = TRUE) && requireNamespace("RSQLite", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "ejemplo", data.frame(id = 1:10, valor = 11:20)) resultado <- perfilar_dbi(con, "ejemplo", muestra = 5, orden_muestra = "id") DBI::dbDisconnect(con) }if (requireNamespace("DBI", quietly = TRUE) && requireNamespace("RSQLite", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "ejemplo", data.frame(id = 1:10, valor = 11:20)) resultado <- perfilar_dbi(con, "ejemplo", muestra = 5, orden_muestra = "id") DBI::dbDisconnect(con) }
Aplica perfilar() a cada grupo de filas por separado y devuelve los
hallazgos de todos los grupos en una sola tabla. Es la respuesta al formato
largo: una tabla donde cada fila describe un atributo distinto no es una
tabla, son muchas apiladas, y perfilarla como si fuera una sola mezcla
dominios que no tienen nada que ver entre sí.
perfilar_por(datos, por, clave = NULL, min_filas = 30L, ...)perfilar_por(datos, por, clave = NULL, min_filas = 30L, ...)
datos |
Data frame a perfilar. Sus nombres de columna tienen que ser
distintos: cada grupo se arma por nombre, y con dos columnas iguales se
perfilaría una en lugar de la otra. Si los hay, la función se detiene;
renómbrelas, por ejemplo con |
por |
Nombre de una columna atómica cuyos valores definen los grupos.
Los ausentes forman un grupo propio, con la etiqueta La etiqueta de cada grupo vuelve a su valor. En una columna de dobles,
Lo mismo vale para las otras clases: un Si la columna es un factor, un nivel declarado sin ninguna fila es un
grupo de cero filas: no se perfila y se declara en |
clave |
Nombres de columnas de identidad que se conservan en cada grupo aunque estén enteramente ausentes. Importa: sin la clave de entidad, el diagnóstico de filas duplicadas informa como duplicada cada repetición del valor del atributo. La declaración viaja al perfilado de cada grupo, igual que si se pasara
|
min_filas |
Grupos con menos filas que este número no se perfilan y se declaran en la cobertura. Tiene que ser un entero positivo: un valor fraccionario se rechaza, en vez de truncarlo y perfilar un grupo que estaba por debajo del umbral. El valor por omisión evita conclusiones sobre grupos donde ningún diagnóstico tiene soporte. |
... |
Argumentos enviados a |
Dentro de cada grupo se descartan las columnas enteramente ausentes antes de
perfilar. En un modelo entidad-atributo-valor bien formado eso deja viva
exactamente la columna de valor que corresponde al atributo del grupo, y es
lo que evita informar como falta lo que es la forma del dato. Las
declaraciones que nombran columnas retiradas se recortan para ese grupo y el
nombre de cada recorte, con su motivo, se declara en
cobertura_grupos$columnas_descartadas. En aplicabilidad también se
recorta una regla si all.vars() de su fórmula menciona una columna original
que no está en la rebanada; una variable que no es columna de la tabla se
conserva porque puede venir legítimamente del entorno de la fórmula.
La función no adivina cuál es la columna de agrupación: la declara quien
conoce el dato, igual que perfilar() no adivina claves ni jerarquías.
Data frame de clase hallazgos_por_grupo con las columnas de
hallazgos de perfilar() precedidas por grupo y n_filas_grupo. El
atributo cobertura_grupos declara los grupos no perfilados, las columnas
enteramente ausentes y las declaraciones recortadas por grupo. Su columna
lógica grupo_perfilado dice de qué clase de grupo habla cada fila: hay
filas sobre grupos que sí se perfilaron —la colisión de "(ausente)",
las columnas descartadas— y filas sobre grupos que no. Sin esa distinción
n_filas_grupo no se podía sumar, porque un grupo perfilado aparecía en los
hallazgos y otra vez en la cobertura.
Cómo reconciliar las filas. Hay una fila por grupo y motivo, así que un
grupo puede aparecer más de una vez; la cuenta se hace sobre grupos
distintos. La suma de n_filas_grupo sobre los grupos distintos —los
perfilados, que están en los hallazgos y en las filas con
grupo_perfilado = TRUE, más los no perfilados— da las filas de la tabla.
Para que la cuenta cierre, un grupo perfilado que no produjo ningún
hallazgo también tiene su fila en cobertura_grupos, con
grupo_perfilado = TRUE y ese motivo: si no, no estaría en ninguna de las
dos tablas.
El atributo etiquetas_personales declara si la columna de agrupación
lleva datos personales. Las etiquetas de grupo son valores de esa
columna, así que la salida los publica —en los hallazgos y en las dos
tablas de cobertura— aunque perfilar() enmascare esa misma columna. No se
enmascaran aquí porque la etiqueta es el eje del resultado y sin ella los
grupos no se distinguen; pero tampoco ocurre en silencio: se avisa al
ejecutar y queda declarado en el objeto. Para que no se publiquen, agrupe
por una columna seudonimizada. Lo mismo, con tipo
contiene_valor_protegido, cuando la columna de agrupación no es personal
pero alguna etiqueta contiene un valor protegido de otra columna —un
proveedor juanperezsrl junto a un titular protegido «Juan Perez»—: la
etiqueta se publica igual, y n_grupos cuenta cuántas lo contienen. Cada
grupo se perfila sobre sus filas, pero la salida entera se barre además con
los valores protegidos de toda la tabla: el titular que en la columna
protegida sólo aparece en un grupo no se publica en la evidencia de otro. El
atributo queda vacío cuando ninguna etiqueta lleva datos personales, y
también cuando proteger_datos_personales es
FALSE, porque entonces ya está declarado que se quieren los valores. Si el
léxico no reconoce el nombre de la columna, decláresela con
columnas_personales: ese argumento —como columnas_opcionales, clave,
columnas_sin_ceros, columnas_no_negativas, columnas_codigo y
aplicabilidad— puede
nombrar la columna de agrupación, y se recorta de lo que se envía a
perfilar() para cada grupo, donde esa columna ya no está. El recorte se
publica por grupo en cobertura_grupos$columnas_descartadas, junto con el
nombre de la declaración y el motivo. Para aplicabilidad, también se
recorta una regla cuando alguna variable de su fórmula —según
all.vars()— es una columna original ausente de la rebanada; las
referencias que no son columnas originales se conservan como referencias
posibles al entorno de la fórmula.
El atributo cobertura_diagnosticos declara, por grupo, los
diagnósticos que no se evaluaron y por qué. Cada grupo se perfila por
separado, así que cada uno declina los suyos: una columna puede tener
bastantes filas en un grupo y muy pocas en otro. Sin esa tabla, un grupo sin
hallazgos se lee como un grupo sano, cuando puede ser un grupo sobre el que
no se miró.
Las senales de numeracion densa y de identificacion se miden tambien sobre
columnas integer64 con comparaciones exactas. Si un grupo vuelve a
conjeturar un centinela por la particion, el hallazgo se mueve a esta
cobertura y no se publica como defecto del grupo. "La columna entera" es la
que perfilar() mide con la misma declaración: las filas que
aplicabilidad deja fuera no cuentan para el centinela, la numeración ni
la guarda de Benford. Y lo que la columna entera sí conjetura —una
numeración cuya moda sobresale, o un candidato fuera de su rango— no es
de la partición: el grupo lo publica.
perfilar(), cobertura_analisis()
largo <- data.frame( entidad = rep(1:40, each = 2), atributo = rep(c("pais", "edad"), 40), valor = c(rbind(sample(c("UY", "AR"), 40, TRUE), as.character(20:59))) ) hallazgos <- perfilar_por(largo, "atributo", clave = "entidad", min_filas = 10) head(hallazgos[, c("grupo", "columna", "tipo_hallazgo")])largo <- data.frame( entidad = rep(1:40, each = 2), atributo = rep(c("pais", "edad"), 40), valor = c(rbind(sample(c("UY", "AR"), 40, TRUE), as.character(20:59))) ) hallazgos <- perfilar_por(largo, "atributo", clave = "entidad", min_filas = 10) head(hallazgos[, c("grupo", "columna", "tipo_hallazgo")])
Persiste un objeto analizar() en RDS con número de esquema. Los datos de
entrada no se guardan por omisión. Las funciones de reglas se sustituyen por
declaraciones pequeñas: una dependencia funcional se reconstruye al leer
sólo si los datos fueron incluidos; una función arbitraria queda desactivada.
Así el archivo no serializa entornos de ejecución completos.
guardar_analisis( x, archivo, incluir_datos = FALSE, proteger_datos_personales = TRUE, sobrescribir = FALSE, comprimir = "xz" ) leer_analisis(archivo)guardar_analisis( x, archivo, incluir_datos = FALSE, proteger_datos_personales = TRUE, sobrescribir = FALSE, comprimir = "xz" ) leer_analisis(archivo)
x |
Objeto creado por |
archivo |
Ruta del archivo RDS. Una ruta que ya es un directorio se rechaza: el archivo va dentro, con su nombre. |
incluir_datos |
Si se persiste la copia de los datos conservada por
|
proteger_datos_personales |
Si se protege toda evidencia derivada. |
sobrescribir |
Si se permite reemplazar un archivo existente. |
comprimir |
Compresión admitida por |
La protección de datos personales con evidencia suficiente se vuelve a aplicar antes de escribir. Incluir datos que contienen columnas protegidas exige desactivar expresamente esa protección; una clasificación débil se conserva como información pero no activa esa restricción.
El archivo se escribe con el formato RDS 2, no con el 3 que saveRDS()
usa por omisión. El 3 anota la codificación nativa de quien escribe y al leer
traduce desde ella el texto que no declara la suya; en Windows esa
traducción no falla y cambia los bytes en silencio, así que un análisis
guardado bajo un locale y leído bajo otro podía volver con el texto alterado.
Ese formato tiene un costo y conviene saberlo: no conserva la
representación compacta de los vectores que R guarda así. Medido, una
columna 1:1e6 ocupa 103 bytes con el formato 3 y 2.127.903 con el 2; con
incluir_datos = TRUE sobre una tabla con una columna id = seq_len(n), eso
son unos 2 MB por cada millón de filas, y a escala la escritura puede exigir
materializar en memoria lo que el formato 3 no materializa. Se eligió así
porque un archivo más grande es visible y recuperable, y un texto corrompido
en silencio no lo es. Quien prefiera la otra relación puede serializar el
objeto por su cuenta.
guardar_analisis() devuelve la ruta de forma invisible;
leer_analisis() devuelve un objeto analisis.
El objeto guardado gana meta$persistencia, que declara qué se guardó y
qué no: version_esquema, si los datos viajan (datos_incluidos), si la
evidencia salió protegida (evidencia_protegida) y cuántas funciones se
sustituyeron por su descripción (funciones_sustituidas). Es el único
campo que un objeto leído tiene y el original no: lo que se guarda no es
idéntico a lo que se analizó, y esa diferencia se declara en vez de
suponerse. Al leer, si hubo funciones sustituidas, advertencias agrega
una fila que declara que la propuesta se volvio a derivar sin ellas y que
puede no coincidir con la decision original, que se conserva.
analizar(), reportar(), guardar_historico()
a <- analizar(datos_administrativos, argumentos_perfil = list( analizar_dependencias = FALSE )) ruta <- tempfile(fileext = ".rds") guardar_analisis(a, ruta) b <- leer_analisis(ruta) unlink(ruta)a <- analizar(datos_administrativos, argumentos_perfil = list( analizar_dependencias = FALSE )) ruta <- tempfile(fileext = ".rds") guardar_analisis(a, ruta) b <- leer_analisis(ruta) unlink(ruta)
perfilar_dbi() antes de pagarloEmite sólo consultas de preparación —leer el esquema y sondear capacidades—
y devuelve cuántas consultas emitiría el perfilado completo, de qué clase y
con qué alcance sobre la tabla. No escanea datos para decidir el costo.
Cuando politica_costo = "por_cardinalidad", una clave estructural exacta
puede cerrar la decisión; si no hay una fuente de catálogo utilizable, el
plan publica el rango entre omitir y ejecutar la moda; la mediana no entra en
ese criterio proporcional. Nunca lanza COUNT(DISTINCT ...) para despejar
esa incertidumbre.
Las fuentes estructurales se resuelven cuando la política necesita la
cardinalidad, aunque estrategia_distintos no permita medirla. La
disponibilidad de la estrategia gobierna la medición, no el conocimiento que
ya da el catálogo.
Para el COUNT(DISTINCT) exacto, la preparación puede consultar además las
estadísticas de PostgreSQL (pg_stats, pg_class.reltuples, SHOW work_mem
y, desde PostgreSQL 13, SHOW hash_mem_multiplier) para estimar el tamaño
del hash y avisar un posible derrame. Esa consulta de metadatos no publica
cardinalidad medida ni reemplaza la medición posterior.
Para la moda y la mediana se publican, además, los atributos
estimacion_derrame_moda y estimacion_derrame_mediana. La moda deriva su
metodo ("hash" o "sort") del primer Aggregate de un
EXPLAIN (FORMAT JSON, COSTS OFF) de la consulta exacta, sin ANALYZE ni
lectura de datos. La mediana siempre modela un sort y distingue su huella de
decisión de la magnitud del tape. Ambas usan n_validos de catálogo en el
plan, pisos de 32 bytes para tipos fijos y 42 para numeric, y dejan el
objeto como no_disponible cuando el catálogo o el motor no permiten una
estimación.
Cuando esa lectura de catálogo trae pg_class.reltuples positivo, el plan lo
reutiliza como una estimación declarada del número de filas. La magnitud y las
proyecciones de trabajo de moda y mediana quedan entonces disponibles y dicen
explícitamente estimado_catalogo; no son duraciones ni mediciones. Un valor
cero o negativo —típico de una relación sin ANALYZE— conserva el estado
sin dato filas. Los demás motores conservan ese estado si no tienen una
lectura de catálogo ya disponible.
plan_perfilado_dbi( conexion, tabla, universo = c("tabla_completa", "muestra_motor"), muestra_motor = NULL, muestra = Inf, orden_muestra = NULL, metricas = .METRICAS_DBI, estrategia_distintos = "exacta", estrategia_mediana = c("exacta", "aproximada_motor"), politica_costo = c("todas", "por_cardinalidad"), bloque_muestra = c("con_muestra", "solo_agregados"), max_consultas = Inf, dialecto = "auto", incluir_valores = TRUE, tamano_lote = NULL, tamano_lote_planos = .TAMANO_LOTE_PLANOS_DBI, tamano_lote_distintos = .TAMANO_LOTE_DISTINTOS_DBI, instrumentar = FALSE, umbral_cardinalidad = .UMBRAL_CARDINALIDAD_COSTO_DBI, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, bloque_filas = NULL, max_bytes_procesamiento = .MAX_BYTES_MUESTRA, max_bytes_materializacion = .MAX_BYTES_MUESTRA )plan_perfilado_dbi( conexion, tabla, universo = c("tabla_completa", "muestra_motor"), muestra_motor = NULL, muestra = Inf, orden_muestra = NULL, metricas = .METRICAS_DBI, estrategia_distintos = "exacta", estrategia_mediana = c("exacta", "aproximada_motor"), politica_costo = c("todas", "por_cardinalidad"), bloque_muestra = c("con_muestra", "solo_agregados"), max_consultas = Inf, dialecto = "auto", incluir_valores = TRUE, tamano_lote = NULL, tamano_lote_planos = .TAMANO_LOTE_PLANOS_DBI, tamano_lote_distintos = .TAMANO_LOTE_DISTINTOS_DBI, instrumentar = FALSE, umbral_cardinalidad = .UMBRAL_CARDINALIDAD_COSTO_DBI, max_celdas_muestra = .MAX_CELDAS_MUESTRA, max_bytes_muestra = .MAX_BYTES_MUESTRA, bloque_filas = NULL, max_bytes_procesamiento = .MAX_BYTES_MUESTRA, max_bytes_materializacion = .MAX_BYTES_MUESTRA )
conexion |
Conexión abierta compatible con DBI. |
tabla |
Nombre de tabla o un objeto aceptado por
|
universo |
Universo sobre el que se calculan los agregados SQL:
|
muestra_motor |
Cantidad positiva y finita de filas que el motor debe
tomar cuando |
muestra |
Cantidad positiva de filas solicitadas para el perfil de
muestra que se trae a R, o Sin |
orden_muestra |
Columnas para |
metricas |
Selección explícita de grupos de métricas: |
estrategia_distintos |
Procedencia explícita para |
estrategia_mediana |
Preferencia para resolver |
politica_costo |
Política optativa para las métricas caras. El valor por
omisión, |
bloque_muestra |
Qué bloques se solicitan: |
max_consultas |
Presupuesto declarado de consultas. Al agotarse, las
métricas restantes quedan en |
dialecto |
Capacidad de acotar filas: |
incluir_valores |
Si el resumen informa valores de celda: moda, mínimo,
máximo y mediana. Con |
tamano_lote |
Cantidad máxima de columnas por consulta consolidada.
Se conserva por compatibilidad y, si se informa, fija el tamaño de las dos
familias. Para control separado, usar |
tamano_lote_planos |
Cantidad máxima de columnas por consulta de agregados planos. El valor por omisión es 20. |
tamano_lote_distintos |
Cantidad máxima de columnas por consulta de
cardinalidades exactas. El valor por omisión es 2, medido sobre el servidor
de referencia: el |
instrumentar |
En el plan, si es |
umbral_cardinalidad |
Proporción entre valores distintos y válidos que
activa la omisión de la moda con |
max_celdas_muestra |
Máximo de celdas que puede contener el bloque
|
max_bytes_muestra |
Máximo de bytes de la muestra materializada que
alimenta |
bloque_filas |
En la via I1, entero positivo que activa la fuente por
bloques de |
max_bytes_procesamiento |
Límite de bytes del estado retenido por el
procesamiento por bloques y por la lectura del spool en R. Se comprueba
antes de publicar un bloque o una pasada; |
max_bytes_materializacion |
Presupuesto de bytes del spool externo de
|
estrategia_distintos declara la procedencia de n_distintos antes de la
corrida y conserva por separado lo pedido, lo resuelto y el estado. No hay
auto: "exacta" es el valor por omisión, "aproximada_motor" queda
no_disponible si el motor no ofrece una función aceptada, "catalogo"
lee pg_stats.n_distinct en PostgreSQL y publica una estimación con estado
estimado_catalogo cuando universo = "tabla_completa", y "omitida" no
emite el agregado. Con universo = "muestra_motor", catalogo queda
no_disponible: sus estadísticas describen la relación entera y no el
subconjunto de la corrida.
Un n_distinct
positivo es un conteo y uno negativo una fracción de las filas. Si la
relación tiene descendientes se usa la fila inherited = TRUE, que describe
la consulta sin ONLY; si no tiene hijas se usa la única fila propia. La
fracción se convierte con la suma de pg_class.reltuples de la jerarquía.
Si falta una estimación utilizable —por ejemplo, antes de ANALYZE— o hay
filas ambiguas, se conserva no_disponible, nunca cero.
fuente_cardinalidad_costo sigue siendo independiente y sólo describe el
número usado por la política de costo cuando esa política se pide.
El plan previo no proyecta segundos para COUNT(DISTINCT): no lee los datos
y, por tanto, no tiene una referencia medida. La única referencia honesta es
el primer lote de distintos de la corrida, pero obtenerla cuesta una
consulta que este planificador no emite. Durante perfilar_dbi(), cuando hay
más de un lote y la instrumentación está activa, esa primera medición se
multiplica por la cantidad de lotes y se publica en
resumen_tabla$meta$costo_distintos. El aviso temporal llega después del
primer lote y antes del segundo; con un solo lote se declara que no hay nada
que proyectar.
La memoria del procesamiento no se estima: no escala de forma predecible con
las filas ni con las celdas, y eso se midió. El atributo
memoria_procesamiento conserva esa declaración, la magnitud conocida del
trabajo (filas, celdas y pares_texto) y referencias medidas de otras
corridas. Esas referencias no son una predicción para la tabla del plan:
traer costó aproximadamente 0,13 GB por millón de filas y procesar en R
aproximadamente 1,0-1,5 MB por cada mil filas, pero esta segunda cifra varió
1,62x entre tablas de la misma magnitud.
Ver todas las filas y tener todas las filas en memoria no son lo mismo. En corridas de referencia, 4,5 millones de filas entraron en 0,6 GB y tardaron 25 segundos, mientras que procesar 4,5 millones ocupó aproximadamente 7 GB y procesar 12,8 millones aproximadamente 19 GB. El problema observado está en el procesamiento en R, no en la red ni en el motor.
Para universo = "muestra_motor", el plan declara una única selección que
se pagará para cerrar el spool externo de sesión cliente. attr(plan, "materializacion") publica pagado = FALSE, backend, versión, presupuesto,
result set, fetches esperados, filas y bytes; attr(plan, "pasadas") declara
que valor, índice y LSH leerán el mismo spool. La referencia medida para
justificar este costo es PostgreSQL 16, 2 millones de filas: 10.000, 100.000
y 500.000 filas dieron 0,448/1,684/5,598 s con spool frente a
0,814/2,178/3,265 s reordenando cada pasada; el cruce está entre 100.000 y
500.000, y la elección prioriza identidad.
Data frame de clase plan_perfilado_dbi con clase_consulta,
n_consultas, n_consultas_max y alcance, y los atributos total,
total_minimo, total_maximo, total_lotes_rechazados, columnas,
columnas_numericas, dialecto, consultas_emitidas, metricas,
metricas_ejecucion, politica_costo, estrategia_distintos,
fuente_cardinalidad_costo, moda_guardian, mediana_consolidada, filas,
filas_fuente, estimacion_filas, proyecciones, mediana_escalar,
tamano_lote_planos, tamano_lote_distintos, estimacion_derrame,
estimacion_derrame_moda, estimacion_derrame_mediana,
celdas, memoria_procesamiento, max_celdas_muestra,
max_bytes_muestra, tope_muestra y muestreo, y,
cuando se pide distintos, supuesto_costo_distintos.
memoria_procesamiento siempre tiene estado = "no_estimada": no es una
estimación de consumo, sino la declaración de su ausencia, el motivo, la
magnitud del trabajo y referencias medidas de otras corridas. El atributo
estimacion_derrame es independiente: sólo describe la estimación del hash
en el motor para COUNT(DISTINCT) y no la memoria del procesamiento en R.
estimacion_derrame_moda y estimacion_derrame_mediana describen,
respectivamente, el nodo real de la moda y el sort de la mediana. Sus
lotes deciden por el máximo de columna; en una mediana consolidada,
estado_io_total_bytes es sólo la suma informativa de los tapes.
muestreo declara si la forma muestreada se pudo construir sin emitir una
consulta de datos. En universo = "muestra_motor", cuando su estado es
"no_disponible", el plan excluye las métricas SQL de esa muestra y
supuesto conserva el motivo.
Cuando se pide bloque_muestra = "solo_agregados", también conserva ese
valor en el atributo bloque_muestra y no incluye la fila de la lectura de
muestra.
Los atributos max_celdas_muestra, max_bytes_muestra y tope_muestra
declaran la cota que se aplicará al bloque perfil_muestra. El plan no lee
datos: resuelve la cota de celdas con el ancho del esquema y declara que la
cota de bytes requiere una sonda de hasta cien filas durante la corrida.
Si la muestra pedida supera la cota de celdas, tope_muestra y print()
lo dicen antes de correr. La fila de consulta de muestra conserva su
conteo separado de los agregados; los topes no cambian ninguna consulta
SQL de resumen.
El costo no se declara como un número sino como un rango: total es el
extremo inferior, que supone que la política omite la moda cuya cardinalidad
no se conoce, y total_maximo el superior, que supone que la ejecuta. La
mediana queda fuera de ese supuesto proporcional y se cuenta según las
columnas numéricas solicitadas. Ambos incluyen la preparación y el perfilado; el rechazo
de lotes puede agregar las sondas de bisección declaradas por
total_lotes_rechazados. El costo real cae entre los extremos cuando
universo es "tabla_completa" o attr(plan, "muestreo")$estado es
"disponible"; en "no_sondeado" la forma sólo se construyó localmente
y el intervalo queda condicionado a que la sonda de la corrida la acepte.
Si la forma muestreada no se puede construir, el plan declara ese caso,
excluye sus métricas del rango y attr(plan, "supuesto") dice por qué.
Cuántas consultas se emiten no dice cuánto cuestan: catorce consultas sobre dos millones de filas son mucho más trabajo que doscientas sobre mil. Por eso el plan estima además la magnitud, y la estima en sus dos mitades, porque el reloj de una corrida no lo decide siempre el motor.
La del motor va en filas_leidas (cuántas filas habría que leer) y
ordenaciones_completas (cuántas veces habría que ordenar la tabla
entera), y se resume en magnitud_motor. La del cliente va en
columnas_texto y pares_texto —cuántos pares de formas podría comparar
en R el detector de vocabulario sobre la muestra— y se resume en
magnitud_texto. magnitud es la mayor de las dos: "baja", "media",
"alta", o "desconocida" si no se conoce el número de filas.
supuesto_costo dice de dónde sale cada cuenta.
El plan previo no publica duraciones, CPU, ni filas o bytes medidos, ni agrega
una proyección temporal de COUNT(DISTINCT). Puede publicar filas estimadas
por pg_class.reltuples y proyecciones de trabajo de moda/mediana, siempre
rotuladas como estimación de catálogo y no como medición. Aunque el plan de una corrida
conserve el atributo supuesto_costo_distintos, la medición y la proyección
sólo aparecen en resumen_tabla$meta$costo_distintos, después de ejecutar
el primer lote. El atributo sólo declara por qué esa proyección no existe
antes de correr; no es una duración ni una estimación temporal.
Si se pide politica_costo = "por_cardinalidad", el plan busca primero una
garantía estructural o una fuente de catálogo. Si la fuente queda
desconocida, no emite un agregado para aclararla: n_consultas omite la
moda y n_consultas_max deja abierto el camino que la ejecuta. La mediana
se conserva porque su costo medido es plano frente a la cardinalidad y está
gobernado por las filas. La corrida mide distintos sólo si la política lo
necesita. La política por omisión es "todas": el paquete no elige por el
usuario.
Una fuente estructural se resuelve aunque la estrategia de distintos este
omitida o no disponible; esta ultima solo gobierna si se puede medir.
El catalogo de la clave primaria se consulta siempre para conservar esa
respuesta en resumen_tabla$meta$clave; es una lectura de metadatos y no
un recorrido de la tabla.
Contar sólo el motor daba juicios falsos con números ciertos: una tabla de
3.912 filas con una columna de geometría en texto pedía 64.592 lecturas
—magnitud "baja"— y tardaba 35 segundos, porque el trabajo estaba en la
comparación de formas, que no es una lectura de fila. El método de
impresión muestra las dos mitades, avisa cuando la magnitud es alta y
nombra las palancas para acotarla, que no son las mismas de un lado que
del otro.
if (requireNamespace("DBI", quietly = TRUE) && requireNamespace("RSQLite", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "ejemplo", data.frame(id = 1:10, valor = 11:20)) plan_perfilado_dbi(con, "ejemplo") DBI::dbDisconnect(con) }if (requireNamespace("DBI", quietly = TRUE) && requireNamespace("RSQLite", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "ejemplo", data.frame(id = 1:10, valor = 11:20)) plan_perfilado_dbi(con, "ejemplo") DBI::dbDisconnect(con) }
planificar_limpieza() transforma los hallazgos de un objeto perfil en un
objeto de datos editable, sin modificar los datos examinados. Cada fila
representa una acción propuesta. Sólo se marcan como recomendadas las
estrategias correctas con independencia del dominio; algunas, como marcar
valores extremos, permanecen inactivas hasta que se decida actuar. Las
decisiones contextuales quedan desactivadas y los formatos de fecha ambiguos
quedan bloqueados.
planificar_limpieza(perfil, datos = NULL, soporte_minimo_dependencia = 2L) aplicar(plan, datos, permitir_eliminacion = FALSE, conservar_eliminados = TRUE)planificar_limpieza(perfil, datos = NULL, soporte_minimo_dependencia = 2L) aplicar(plan, datos, permitir_eliminacion = FALSE, conservar_eliminados = TRUE)
perfil |
Objeto de clase |
datos |
|
soporte_minimo_dependencia |
Cantidad mínima de observaciones concordantes por valor determinante para proponer una imputación. |
plan |
Objeto de clase |
permitir_eliminacion |
Segundo consentimiento obligatorio para ejecutar
acciones que eliminan filas o columnas. Sin él, una acción eliminatoria
seleccionada hace que |
conservar_eliminados |
Si se conservan en el resultado las filas y
columnas retiradas. Es |
aplicar() ejecuta exclusivamente las filas con aplicar == TRUE, sobre una
copia de datos. Verifica que cada columna siga siendo identificable y que
las conversiones sean completas antes de sustituirla. Devuelve los datos
nuevos junto con un registro de las acciones y sus parámetros. El mismo
registro queda en el atributo registro_limpieza de los datos resultantes.
Las alternativas para un mismo hallazgo comparten grupo; las acciones
independientes usan NA. Como máximo una alternativa de cada grupo puede
tener aplicar == TRUE, invariante que aplicar() vuelve a validar. No se
agrega una fila ficticia para "no hacer nada": decision_grupo distingue
pendiente, recomendada, desactivada, elegida y omitida, mientras
recomendacion_grupo = "no_hacer_nada" representa una recomendación
explícita de conservar los datos. Esto permite separar un grupo aún no
revisado de una omisión deliberada.
estado distingue acciones lista, bloqueada e informativa; orden
fija la secuencia reproducible. Si dos acciones comparten el mismo orden,
el empate lo resuelve id_accion, no la posición de la fila: reordenar el
plan no cambia el resultado. Y un grupo marcado como elegida sin ninguna
acción activa se rechaza, porque no es ni una elección ni una omisión. n_afectadas es la estimación del perfil
sobre lo que esta acción tocaría, que puede ser menos que el conteo del
hallazgo que la originó cuando la acción sólo cubre parte del caso: una
columna con tres valores de codificación rota, de los cuales uno es
reparable, produce un hallazgo con n_afectados = 3 y una acción
reparar_codificacion con n_afectadas = 1. Las dos cifras son ciertas y
cuentan cosas distintas. unidad_conteo dice si cuenta filas, columnas o
valores distintos —lo
declara la acción cuando cuenta en una unidad propia, y sólo si no lo hace se
hereda del hallazgo—. El registro informa n_cambiadas sobre los datos
recibidos.
n_afectadas y n_cambiadas pueden no coincidir, y las dos son ciertas.
La estimación se calcula sobre los datos que se perfilaron; el registro
cuenta lo que pasó al aplicar. Si una acción anterior del mismo plan ya tocó
esa columna, la posterior encuentra menos —o más— de lo estimado: con
convertir_sentinelas_numericos (orden 110) convirtiendo tres -999 en
ausentes, winsorizar_outliers (orden 520) recorta dos valores donde el plan
estimaba siete. No es un desvío que ocultar: orden, n_afectadas y
n_cambiadas se publican los tres, y compararlos es la forma de ver el
efecto de la composición. Sólo el caso extremo —la acción no produce ningún
efecto —también cuando el plan no trae una estimación válida— se registra
como fallida con su motivo.
Esa comparación sólo lee composición cuando las dos cifras cuentan en la
misma unidad. n_afectadas cuenta en la unidad_conteo que el plan
declara y n_cambiadas cuenta lo que la acción tocó al aplicarse, que es
su unidad natural. Cuando la acción declara unidad_conteo = "valor_distinto" —las conversiones de mayúsculas y minúsculas—, las dos
cifras miden poblaciones distintas y su diferencia no dice nada sobre la
composición: medido sobre c("Ana", "ana", "ANA", "Beto", " Ana "),
convertir_minusculas estima n_afectadas = 3 valores distintos y el
registro publica n_cambiadas = 4 celdas. Las dos son ciertas. La unidad
del plan se recupera uniendo el registro con el plan por id_accion, que
los dos publican. reversible
indica si la conversión conserva la identidad de cada valor. Las
conversiones se comprueban sobre todos los valores de datos: las numéricas
bloquean ceros iniciales, colisiones no inyectivas y valores con más cifras
significativas de las que guarda un número de doble precisión —un decimal se
compara con la precisión con que vino escrito—, mientras que fechas,
fechas-hora y lógicos sólo bloquean conversiones no ejecutables o no
inyectivas. Las fechas pueden cambiar a la representación canónica del tipo
sin que eso sea una pérdida. Sin datos no se puede hacer la comprobación y
la acción queda bloqueada. Cuando una conversión de tipo se ejecuta y no
es reversible se marca destructiva —también si la columna no era segura y
se activa a mano—, no se activa por defecto y el registro conserva
n_no_reversibles y la justificación de la decisión.
destructiva no es sinónimo de "pierde algo": marca las acciones que el
usuario tiene que activar a mano —las que retiran filas o columnas y las
conversiones que pierden representación, como winsorizar_outliers sobre una
columna guardada como entera, donde los límites de Tukey son cuartiles y
la columna queda en doble precisión: la justificación lo dice y parametros
publica tipo_original y tipo_resultante. Sobre cualquier otra clase
—texto con números, factor, fecha— la acción conserva la clase y reescribe
sólo las celdas recortadas; Inf, NaN y NA quedan como estaban— y por
eso ninguna acción
destructiva puede estar recomendada. Hay acciones recomendadas que sí
pierden el valor de una celda: convertir_ausencias_textuales cambia un
marcador por NA y eliminar_controles_invisibles quita un carácter. Esas
lo dicen en su justificación y el registro las cuantifica en
n_no_reversibles; leer destructiva = FALSE no significa que no se haya
perdido nada, sino que el paquete pudo recomendar la acción sin conocer el
dominio.
Sobre una columna factor las acciones por celda devuelven texto: el
resultado no puede ser un factor incompleto, así que el orden declarado y los
niveles sin observaciones no se conservan. La justificación de la acción lo
dice, y si el factor es ordenado la acción queda recomendada pero sin
activar, porque conservar el orden es una decisión del dominio.
Tres atributos del plan declaran lo que el plan no cubre.
cobertura_diagnosticos trae los diagnósticos que el perfil no pudo
evaluar, para que leer tres acciones no se confunda con "lo demás está
bien". hallazgos_sin_accion_por_columna_ambigua trae los hallazgos medidos
que no produjeron acción porque su columna comparte nombre con otra y no hay
forma de saber sobre cuál actuaría la limpieza; el remedio es normalizar los
nombres y volver a perfilar. hallazgos_sin_accion trae el resto: los
hallazgos que el perfil midió, con su columna identificada, y que ninguna
acción del plan atiende —porque el plan no tiene una estrategia para ese
tipo, o porque la que tiene necesitaba una condición que no se cumplió—. Su
motivo dice si la misma columna recibió otras acciones, sin afirmar que
alguna cubra el hallazgo: eso es una decisión del dominio. Los tres se
imprimen con el plan.
convertir_numero_regional sólo se recomienda si todos los valores
presentes comparten convención decimal, unidad y moneda. Un valor con %
se divide por 100 y queda como proporción en [0, 1], la escala con que el
paquete publica toda proporción; una unidad o un símbolo de moneda dejan de
formar parte del valor y quedan en parametros. La justificación lo dice
cuando ocurre. Una columna que mezcla 5 con 5 % no se convierte: habría
que decidir si el número sin % está en la misma escala.
La acción de codificación prueba las tablas congeladas de varias
codificaciones y deja en estado_reparacion uno de reparado,
reparado_parcialmente o no_se_pudo. Una reparación parcial —una celda que
el motor repara sólo en parte— no se activa automáticamente: la acción queda
lista con aplicar = FALSE, y se selecciona de forma explícita marcando
aplicar = TRUE; entonces se aplican también las parciales. Una celda sin
reparación local —con el carácter de reemplazo U+FFFD, o que el motor no
pudo reparar— no frena la de las demás: queda como está y la justificación
de la acción la cuenta. La estrategia se llama reparar_codificacion y no
limita el motor a latin-1. Un texto legítimo que se escribe con los mismos
bytes que un mojibake —por ejemplo, uno que cita literalmente la à seguida
de ©— no se distingue de uno roto: se acusa, la evidencia del plan lo
muestra antes de aplicar, y si se aplica la acción se reescribe como la letra
que esos bytes codifican. Si la columna contiene texto así, no active la
acción.
Unos mismos bytes pueden recibir dos diagnósticos: c2 80 es a la vez un
carácter de control C1 y la huella de un € de Windows-1252 leído como
latin-1. Cuando el plan propone reparar la codificación y eliminar ese
control, manda la reparación —corre primero y restituye el €—, y la
eliminación queda sin nada que hacer. Es la lectura habitual en texto real;
en el caso raro de un control C1 genuino, la reparación lo convierte en €.
Una celda cuyo texto no se puede leer —declarada bytes con bytes que no
son UTF-8, o sin marca en una sesión UTF-8, que es lo que deja read.csv()
sin fileEncoding sobre un archivo latin1— no se transforma: el perfil no la
midió y la informa como codificacion_invalida, cuyo remedio es volver a leer
la fuente declarando su codificación. Las acciones de texto trabajan sobre las
demás celdas de la columna y cuentan sólo lo que cambiaron. Una celda sin
marca cuyos bytes son UTF-8 válido se trata como UTF-8 en cualquier locale,
igual que en el perfil: con LC_CTYPE=C las acciones hacen lo mismo que en
una sesión UTF-8.
Una celda latin1 sí se transforma —ahí R conoce la codificación y la
convierte sin pérdida—, y el resultado queda marcado UTF-8. Como la acción
toca sólo las celdas que cambia, una columna latin1 puede quedar con marcas
mixtas: las celdas transformadas en UTF-8 y las intactas en latin1. El
valor es el mismo, pero los bytes no, así que el registro lo cuenta en
n_codificacion_normalizada en vez de dejarlo implícito. Ese conteo es de
recodificaciones, no de marcas: una celda sin marca cuyos bytes son UTF-8
válido ya es UTF-8 y no cambia de codificación aunque la acción la devuelva
marcada, y un texto ASCII se escribe igual en toda codificación —decodificar
José cambia el valor, no la codificación—. No se convierte la
columna entera a propósito: eso tocaría celdas que el plan no declaró como
cambiadas y haría mentir a n_cambiadas por el otro lado.
Si se marca una acción que no está lista, aplicar() aborta antes de
modificar la copia y enumera las filas problemáticas. Una acción que sí está
lista pero falla se registra con su error y no impide aplicar las siguientes:
cada una conserva atomicidad sobre su propia columna o tabla. Las acciones
que efectivamente eliminan filas o columnas requieren además
permitir_eliminacion = TRUE; una conversión destructiva requiere selección
explícita y deja la pérdida cuantificada. Por defecto, el resultado conserva
lo retirado en eliminados; use conservar_eliminados = FALSE para evitar
ese costo de memoria.
Los hallazgos controles_invisibles, entidades_html y separadores_en_campo
tienen acciones separadas. La detección de invisibles informa tanto los
caracteres que se pueden normalizar como los ZWJ/ZWNJ significativos; la
normalización actúa sobre un conjunto más pequeño que la detección. La acción
eliminar_controles_invisibles quita controles C0/C1 que no son separadores
y los invisibles Unicode de transporte, y se recomienda por defecto; conserva
ZWJ/ZWNJ. normalizar_espacios_invisibles colapsa espacios Unicode (incluido
NBSP) a un espacio ASCII y, como su hermana, se recomienda y se activa por
defecto: queda un espacio, así que el valor sigue ahí; si al colapsarlo dos
valores que eran distintos quedan iguales, el registro lo cuenta en
n_no_reversibles. decodificar_entidades_html cubre las entidades
con nombre comunes en español y referencias numéricas válidas, pero no se
activa sola porque un ampersand puede ser contenido legítimo. Decodifica
una capa: un texto codificado dos veces —&amp;— queda en &,
y el perfil del resultado lo vuelve a señalar. No se itera hasta el fondo
porque no se puede distinguir una doble codificación de un texto que quería
decir &: la segunda capa se decide viéndola.
reemplazar_separadores convierte tabulaciones, saltos de línea, avances de
página y tabulaciones verticales (\\t, \\n, \\r, \\r\\n, \\f y \\v)
en un espacio y también requiere una decisión explícita. Son exactamente los
códigos 9 a 13 que cuenta el hallazgo: los separadores Unicode U+2028 y
U+2029 son de normalizar_espacios_invisibles y el control U+0085 de
eliminar_controles_invisibles, y sobre celdas bytes la acción no toca
ningún byte de un carácter multibyte. Las tres acciones
registran el número de valores cambiados. Una comparación aproximada con
normalizar = TRUE usa estas mismas clases: colapsa espacios y omite basura
de transporte, pero conserva ZWJ/ZWNJ.
Las imputaciones por dependencia funcional se ofrecen desactivadas. Aunque
una dependencia exacta permite deducir un valor sin usar media, moda o un
modelo externo, sigue siendo una regularidad aprendida de una sola entrega y
puede reflejar un error sistemático en vez de una regla de negocio. El plan
conserva el mapa y su soporte para que el usuario la confirme; sólo entonces
se aplica y se vuelve a validar contra los datos recibidos. Si la protección
enmascaró alguna celda del mapa —una clave o un valor, también cuando ninguna
de las dos columnas es personal pero una celda contiene un valor protegido,
como la sucursal que se llama igual que un titular—, queda marcado
mapa_enmascarado = TRUE y se valida igual que sin protección: las filas
que el plan publica en claro son el mapa tal cual, y una fila tapada se
resuelve con el par de los datos recibidos que tiene su forma y que, tapado
con los valores protegidos de esos mismos datos, da lo que el plan publicó;
la relación de los datos se toma con el soporte declarado, sin publicar sus
valores. Si los datos contradicen una fila —la publicada en claro o la
tapada—, la acción queda fallida, y una clave que no está en el plan no
se imputa. Se reconstruía entera sobre los datos recibidos, y sobre otra
entrega escribía valores que el plan nunca mostró. Una fila tapada no se
puede validar si los datos recibidos no traen el valor protegido que la
tapó —un subconjunto sin ese titular—: también queda fallida, con el
motivo, y se aplica planificando sobre esos datos. Y si los datos piden
imputar una clave cuya fila el plan publicó tapada pero no traen ningún par
de esa clave que la resuelva —la entrega que trae sólo las filas a
completar—, la acción tampoco escribe a medias: queda fallida, con el
motivo, porque sin la protección el plan escribiría esas celdas y con la
fila tapada no se sabe con qué. Con una regla de aplicabilidad sobre la columna
dependiente, la imputación respeta su universo como las demás acciones por
celda: no llena las filas declaradas fuera, que son vacío por diseño, y
n_afectadas las excluye.
Sobre una columna protegida, convertir_sentinelas_numericos publica el
catálogo de centinelas del paquete tal cual —es vocabulario, no un dato— y
tapa el resto. Lo tapado se marca con valores_enmascarados = TRUE y
aplicar() lo resuelve sobre los datos que recibe: son los rellenos de la
columna, el dígito repetido que ocupa el lugar de un documento. El plan
ofrece la acción así sólo si esa resolución convierte, sobre los datos del
plan, exactamente las celdas que estima; si no, la acción queda bloqueada
y la justificación dice cómo ejecutarla. Lo mismo en una columna que no es
personal cuando el centinela declarado aparece en una protegida —el
99999999 que se repite en la cédula de una tabla de movimientos—: no se
publica, se declara y se resuelve con los rellenos de esa columna, o la
acción queda bloqueada. Un centinela declarado que no aparece en ninguna
protegida se publica: es la declaración del usuario.
marcar_filas_duplicadas añade dos columnas. .fila_duplicada reproduce la
semántica de duplicated() y marca sólo las apariciones posteriores;
.grupo_duplicado identifica a todas las filas que participan en cada
grupo de contenido idéntico. Las acciones de duplicados comparan las filas
con la misma igualdad que el perfil, también con columnas integer64,
matrices, data.frame anidados o POSIXlt; una tabla con columnas de lista
deja las acciones bloqueadas. Las de columnas duplicadas comparan el
contenido como el perfil: un factor con nivel NA y el mismo factor sin ese
nivel, o un POSIXlt y su POSIXct, son la misma columna también al
aplicar. Marcar no elimina filas. Con las dos columnas
incluidas, las marcas distinguen la primera aparición de las repetidas, pero
no a las repetidas entre sí: en un grupo de tres o más filas un perfil
posterior sigue contando duplicadas. Para saber si los duplicados siguen en
la tabla hay que quitar las columnas de marca antes de perfilarla.
Al volver a planificar sobre lo aplicado, el plan no propone acciones sobre
las marcas que agrega —.fila_duplicada, .grupo_duplicado,
.ausente_<columna> y .outlier_<columna>—: sus hallazgos quedan en
hallazgos_sin_accion con ese motivo, porque el NA de .grupo_duplicado
significa que la fila no participa de ningún grupo y no una ausencia. Una
acción cuya columna de marca ya existe en los datos queda bloqueada:
aplicar() no puede volver a crearla.
El orden operativo se aparta deliberadamente de la secuencia dimensional
frescura–completitud–exactitud–consistencia–unicidad sugerida por el marco.
Primero marca duplicados sin borrar, luego normaliza ausencias y texto, y
deja los cambios de esquema para el final. Esto evita perder la evidencia
original, permite imputar antes de convertir tipos y mantiene identificables
las columnas durante todo el plan. Las eliminaciones nunca se activan por
defecto, por lo que deduplicar temprano no puede hacer desaparecer registros.
destructiva también marca una conversión que pierde representación, aunque
no elimine filas o columnas. El consentimiento permitir_eliminacion sólo
se exige para las estrategias que efectivamente retiran filas o columnas;
una conversión destructiva requiere que el usuario la active explícitamente
y deja su pérdida cuantificada en el registro.
Marcar o eliminar ausentes va después de normalizarlos, no antes. Una
columna con "N/A" y "sin dato" tiene ausentes disfrazados que
convertir_ausencias_textuales convierte en NA reales; si la marca
corriera primero, quedaría una columna .ausente_x que dice FALSE en
filas que terminan en NA —una afirmación falsa sobre los datos, publicada
con las dos acciones en ejecutada y sin error—. Medido sobre siete formas
de tabla antes de corregirlo, tres producían esa marca falsa siguiendo el
idioma documentado plan$aplicar <- plan$recomendada. Por la misma razón,
eliminar_filas_ausentes también va después: eliminar antes deja sin
eliminar las filas cuyo ausente todavía estaba disfrazado.
convertir_ausencias_textuales convierte exactamente los textos de
parametros$valores, comparados sin mayúsculas ni espacios al borde. El
plan arma la lista con los marcadores que el perfil detectó y los que se
declararon en cadenas_ausencia; editarla es una decisión del usuario:
quitar un texto lo conserva y agregar uno lo convierte, como si se hubiera
declarado. Con protección, la lista y la declaración guardan su valor
también sobre una columna protegida: un marcador del catálogo —sin dato—
o uno declarado —SIN REGISTRO— no es el dato de nadie, y la declaración ya
se publica en meta$cadenas_ausencia. Salían [valor protegido] y
aplicar() registraba fallida la acción recomendada.
planificar_limpieza() devuelve un data frame de clase
plan_limpieza. aplicar() devuelve una lista de clase
resultado_limpieza con datos, registro, plan_aplicado, el plan
sincronizado y eliminados. El registro conserva estado (ejecutada
o fallida), error, n_no_reversibles y la justificacion de cada
acción seleccionada, incluso cuando una falla y las siguientes continúan.
n_codificacion_normalizada cuenta, por acción, las celdas cuya marca de
codificación cambió: es el caso de una acción de texto sobre celdas
latin1, cuyo valor se conserva y cuyos bytes no. No cuenta una celda sin
marca cuyos bytes ya eran UTF-8 válido ni un texto ASCII, así que no
depende del locale.
n_no_reversibles cuenta las celdas cuyo VALOR se perdió y no se puede
recuperar desde el resultado: un centinela -999 que pasa a ausencia, un
extremo recortado a su límite, un marcador de ausencia convertido, o un
número que se redondeó al convertirlo. Las acciones que normalizan la
escritura —recortar espacios, quitar un invisible de transporte,
reemplazar separadores o cambiar mayúsculas— dejan el valor en su lugar y
no cuentan, salvo cuando la normalización fusiona valores que eran
distintos: ahí lo que los separaba no queda en ningún lado y esas celdas
sí se cuentan. El criterio se mide sobre el resultado, no por el nombre de
la acción: quitar un guion suave de PRO<U+00AD>DUCTO-A no pierde nada,
y quitar un espacio de ancho cero que distinguía dos claves fusiona las
dos.
Si una acción seleccionada no produce ningún efecto, se registra como
fallida con el motivo y su copia no se incorpora al resultado. La
comprobación del efecto observa n_cambiadas aunque n_afectadas sea
NA o haya sido editado: una acción seleccionada que no cambia nada queda
fallida, porque la ausencia de efecto es observable sin depender de la
estimación.
Si una columna de entrada es un factor, las acciones que transforman su
texto devuelven una columna character: no se reconstruyen los niveles
originales, porque una limpieza puede introducir valores nuevos.
Los atributos de cada columna —la etiqueta de variable que deja haven—
sobreviven a las acciones que la reescriben, a las conversiones de tipo y a
las que quitan filas; quitar filas de un data.table conserva su clave.
perfilar(), guiar_limpieza(), detectar_dependencias()
datos <- data.frame(categoria = c(" A", "S/D", "B")) perfil <- perfilar(datos) plan <- planificar_limpieza(perfil, datos) plan[, c("grupo", "estrategia", "recomendada", "aplicar")] resultado <- aplicar(plan, datos) resultado$datosdatos <- data.frame(categoria = c(" A", "S/D", "B")) perfil <- perfilar(datos) plan <- planificar_limpieza(perfil, datos) plan[, c("grupo", "estrategia", "recomendada", "aplicar")] resultado <- aplicar(plan, datos) resultado$datos
Devuelve, en una tabla, lo que una regla declara: a qué métricas se engancha, en qué nivel evalúa, qué desenlace produce y qué umbrales usa. Los umbrales viajan aparte de la condición justamente para esto: encerrados en el closure quedaban invisibles y obligaban a escribir otra regla para mover un número.
propiedades_regla(regla)propiedades_regla(regla)
regla |
Objeto creado por |
Es la contraparte de propiedades_metrica(), que describe métricas. Un
umbral pertenece a una regla, no a una métrica, así que no cabía allí.
Data frame con una fila por propiedad: propiedad, valor.
regla_evaluacion(), propiedades_metrica(), evaluar()
regla <- regla_evaluacion( "cobertura minima", function(x, minimo) x >= minimo, umbrales = list(minimo = 0.9) ) propiedades_regla(regla)regla <- regla_evaluacion( "cobertura minima", function(x, minimo) x >= minimo, umbrales = list(minimo = 0.9) ) propiedades_regla(regla)
Construye el puente entre el examen de datos y la medición. El resultado no
ejecuta métricas: es una tabla editable donde cada fila conserva el hallazgo
o diagnóstico que la originó, la justificación, la configuración y una marca
incluir. modelo_desde_propuesta() materializa únicamente las filas que el
usuario deja activas.
proponer_modelo( perfil, datos = NULL, relaciones = NULL, entidades_relacion = character(), max_valores_dominio = 20L, max_sugerencias = 100L )proponer_modelo( perfil, datos = NULL, relaciones = NULL, entidades_relacion = character(), max_valores_dominio = 20L, max_sugerencias = 100L )
perfil |
Objeto creado por |
datos |
Datos originales opcionales. Son necesarios para proponer
dominios observados, materializar reglas de dependencia funcional e
identificar los faltantes disfrazados que cuenta |
relaciones |
Resultado opcional de |
entidades_relacion |
Dos nombres de entidad correspondientes a las
tablas usadas en |
max_valores_dominio |
Máximo de valores para proponer un dominio por extensión. |
max_sugerencias |
Máximo de filas devueltas. |
Las reglas observadas que pueden sobreajustarse a una entrega —dominios y
patrones dominantes— se proponen inactivas. Los controles estructurales
directos, como NoNulo, duplicación exacta y dependencias funcionales
exactas, se activan. max_sugerencias recorta después de ordenar por
prioridad y el objeto declara el total en sus atributos. Dentro de cada
prioridad las filas se ordenan por métrica, entidad y atributos según sus
bytes —no según la intercalación del locale—: el mismo orden en cualquier
sesión.
id_sugerencia se arma con el contenido de la sugerencia, nunca con su
posición: la métrica y sus atributos en su orden —NoNulo:edad,
ReglaIntegridadIntraEntidad:depto,codigo— y, cuando la sugerencia liga dos
tablas, cada atributo con su entidad delante
(ReglaIntegridadInterEntidad:personas.id,visitas.persona_id). Una
sugerencia sin atributos lleva sólo la métrica (EntidadDuplicada). Así una
columna que se agrega o se quita no cambia el id de las demás, y una regla de
regla_evaluacion() que lo nombra sigue midiendo lo mismo en la corrida
siguiente: modelo_desde_propuesta() lo usa como nombre de la instancia, que
es la metrica_instanciada de medir(), del tablero y del histórico. La
entidad de una sugerencia de una sola tabla no entra, porque es el nombre del
perfil y cambia con la expresión perfilada; la configuración tampoco: un
cambio de patrón o de dominio lo declara el histórico en
configuracion_modelo. Dentro de cada nombre se escribe %25, %2C y %23
en lugar de %, , y # —y %2E en lugar de . en una entidad—, y un
nombre NA se escribe %NA, de modo que dos contenidos distintos no dan el
mismo id. Un nombre que no es UTF-8 válido escribe cada byte alto como %XX
—los bytes latin1 de «notaé» sin declarar dan nota%E9—, así que el id es
siempre texto UTF-8 o ASCII, el mismo en cualquier locale. Si dos sugerencias
tienen el mismo contenido —una tabla que repite
el nombre de una columna—, la segunda lleva #2 y la tercera #3, en el
orden de las columnas de la tabla.
Si una columna contiene faltantes disfrazados, NoNulo se propone activa,
con estado lista, y se mide contándolos: los valores que el perfil contó
como disfrazados —con la misma regla y la misma declaración de
sentinelas_numericos y cadenas_ausencia— se declaran en valores_nulos de
su configuracion, y la justificación lo dice. Sobre cuarenta valores con un
NA y tres -999 mide 0,90, no 0,975. Si no se pueden identificar —sin
datos, la propuesta no tiene de dónde sacarlos—, se mide igual con los
valores crudos, y la justificación dice que cuenta sólo los NA y que
sobrestima la completitud. analizar() la mide y modelo_desde_propuesta()
la materializa como a cualquier otra sugerencia activa. Si la columna se
declaró en columnas_opcionales de perfilar(), la NoNulo lleva además
opcional = TRUE y se mide sobre las celdas presentes, el mismo universo
que el perfil: con un NA y tres -999 en cuarenta celdas mide
36/39 = 0,923, que es 1 - prop_faltantes_totales.
Una sugerencia sobre una columna cuyo nombre la tabla repite sale inactiva y
con estado nombre_repetido: una métrica se liga por nombre, y ese nombre no
dice cuál de las columnas mide. modelo_desde_propuesta() no la materializa
y analizar() no la mide hasta que las columnas se renombren.
Data frame S3 de clase propuesta_modelo; las columnas de listas
contienen la configuración y los vínculos sin convertirlos en texto. Su
método print() ya no falla por codificación en ningún locale: la copia
que se exhibe convierte lo que trae codificación declarada, declara como
UTF-8 los bytes válidos sin declarar —R los escapa como <U+00F1> donde el
locale no los represente— y deja el resto tal cual mientras R pueda
imprimirlo. Bajo un locale UTF-8 la salida es exactamente la de R; bajo uno
que no puede representar un carácter difiere a propósito, publicando el
punto de código donde R escaparía los bytes. Sólo cuando R no puede
imprimir se reintenta con su propio octal, \377. Eso no convierte a print() en infalible: si una
clase suya tiene un método format() que da error, ese error le llega sin
alterar. Y un format() que consulte Encoding() verá las marcas de la
copia, no las del objeto original.
perfilar(), detectar_dependencias(), modelo_desde_propuesta(),
modelo()
datos <- data.frame( codigo = rep(1:3, each = 4), categoria = rep(c("A", "B", "C"), each = 4), valor = c(1:11, NA) ) propuesta <- proponer_modelo(perfilar(datos), datos) propuesta[, c("metrica", "origen", "incluir")]datos <- data.frame( codigo = rep(1:3, each = 4), categoria = rep(c("A", "B", "C"), each = 4), valor = c(1:11, NA) ) propuesta <- proponer_modelo(perfilar(datos), datos) propuesta[, c("metrica", "origen", "incluir")]
Un referencial representa conocimiento externo mediante una clave y, de
forma opcional, valores asociados a ella. normalizar controla la
representación usada para emparejar referenciales; no modifica los datos
guardados. La declaración de completitud es explícita: RatioCobertura
exige completo = TRUE y un alcance explícito; un referencial parcial sólo
puede usarse para correctitud.
referencial( datos, clave, valor = character(), completo = FALSE, alcance = NULL, nombre = NULL, normalizar = TRUE )referencial( datos, clave, valor = character(), completo = FALSE, alcance = NULL, nombre = NULL, normalizar = TRUE )
datos |
Tabla de referencia. Se conserva una copia ordinaria de R. |
clave |
Columnas que identifican unívocamente cada fila. Puede contener varias columnas. |
valor |
Columnas asociadas que pueden contrastarse en correctitud débil. Es opcional. |
completo |
Si la tabla cubre todo el universo declarado. Es |
alcance |
Descripción obligatoria cuando |
nombre |
Nombre legible del referencial. Si se omite, usa el nombre del
objeto de entrada o |
normalizar |
|
La clave no admite ausentes y debe identificar cada fila de forma única.
valor no puede repetir columnas de clave y representa atributos que se
contrastan en correctitud semántica débil. El constructor copia la tabla y
no consulta fuentes externas.
Objeto de clase referencial: una lista con la tabla de
referencia en datos, los nombres de columna clave y valor, el
indicador completo -si la referencia agota el dominio-, el alcance
declarado o NA, el nombre, y el perfil de normalización resuelto en
normalizar. Declara la referencia; no compara nada todavía.
metricas_referencial(), instanciar(), detectar_relaciones()
padron <- referencial( data.frame(codigo = c("01", "02"), departamento = c("Artigas", "Canelones")), clave = "codigo", valor = "departamento", completo = TRUE, alcance = "departamentos del Uruguay" ) padronpadron <- referencial( data.frame(codigo = c("01", "02"), departamento = c("Artigas", "Canelones")), clave = "codigo", valor = "departamento", completo = TRUE, alcance = "departamentos del Uruguay" ) padron
Una regla aplica una condición a los resultados de una o más métricas instanciadas. Un perfil reúne reglas y su evaluación es la media aritmética simple de las evaluaciones de esas reglas; no es un índice de dimensión ni un índice global de calidad.
regla_evaluacion( nombre, condicion, metricas = NULL, proporcion_minima = NULL, desenlace = NULL, umbrales = list() ) perfil_evaluacion(nombre, ...) perfiles_madurez(metricas = NULL, umbrales = NULL)regla_evaluacion( nombre, condicion, metricas = NULL, proporcion_minima = NULL, desenlace = NULL, umbrales = list() ) perfil_evaluacion(nombre, ...) perfiles_madurez(metricas = NULL, umbrales = NULL)
nombre |
Nombre de la regla o del perfil. |
condicion |
Función que recibe el vector |
metricas |
Nombres de métricas instanciadas a las que se aplica la
regla, es decir, valores de la columna |
proporcion_minima |
|
desenlace |
|
umbrales |
Vector numérico con nombres, estrictamente creciente y en
|
... |
Reglas creadas por |
perfiles_madurez() crea por omisión los perfiles Básico, Intermedio y
Avanzado de AGESIC, con condiciones estrictas > 0.5, > 0.7 y > 0.9.
El argumento umbrales permite construir otra familia con nombres y cortes
crecientes propios sobre las mismas métricas instanciadas. Los cortes son
umbrales sobre el resultado agregado de cada métrica —una proporción en
[0, 1], lo que deja agregar()—: sobre las medidas por fila de medir(),
que valen 0 o 1, los tres cortes son la misma condición y los tres
perfiles dan la misma cifra, sin aviso. Es lo que hace analizar(), que
evalúa la medición agregada.
regla_evaluacion() almacena la función sin ejecutarla. evaluar()
selecciona las medidas mediante metricas, llama una vez a condicion y
rechaza resultados que no sean lógicos, que tengan otra longitud o que
contengan NA. Si proporcion_minima no es NULL, calcula sobre esos
mismos lógicos la proporción que cumple y la compara mediante >= con el
umbral declarado; no pondera medidas ni construye un puntaje global. Las
evaluaciones cuyas reglas no declaran desenlace conservan su estructura
anterior. Cuando una regla declara desenlace = "suprimir", evaluar()
añade un plan trazable con una fila por medida incumplida y por regla; no
modifica la medición ni los datos que la originaron. La función expresa un
criterio de evaluación; no es un método de medición ni recibe el data frame
original. Si ningún nombre de
metricas coincide, el error enumera tanto los nombres solicitados como
las métricas instanciadas disponibles, que normalmente tienen la forma
[email protected].
regla_evaluacion() devuelve una regla_evaluacion;
perfil_evaluacion() devuelve un perfil_evaluacion; y
perfiles_madurez() devuelve una lista de perfiles.
medir(), evaluar(), perfiles_madurez()
regla_evaluacion(), comparar_evaluaciones(),
historico_calidad()
evaluar(), detectar_deriva_calidad()
regla <- regla_evaluacion("Completitud suficiente", function(x) x > 0.9) regla_70 <- regla_evaluacion( "Al menos 70 %", function(x) x > 0.9, proporcion_minima = 0.7 ) regla_publicacion <- regla_evaluacion( "Medida publicable", function(x) x > 0.9, desenlace = "suprimir" ) perfil <- perfil_evaluacion("Operativo", regla) madurez <- perfiles_madurez("NoNulo") propios <- perfiles_madurez( "NoNulo", c(Exploratorio = 0.3, Operativo = 0.65, Consolidado = 0.85) ) names(madurez) names(propios) perfil$nombreregla <- regla_evaluacion("Completitud suficiente", function(x) x > 0.9) regla_70 <- regla_evaluacion( "Al menos 70 %", function(x) x > 0.9, proporcion_minima = 0.7 ) regla_publicacion <- regla_evaluacion( "Medida publicable", function(x) x > 0.9, desenlace = "suprimir" ) perfil <- perfil_evaluacion("Operativo", regla) madurez <- perfiles_madurez("NoNulo") propios <- perfiles_madurez( "NoNulo", c(Exploratorio = 0.3, Operativo = 0.65, Consolidado = 0.85) ) names(madurez) names(propios) perfil$nombre
Corre detectar_relaciones() sobre los pares de tablas que se declaren, y
devuelve las relaciones candidatas junto con la cobertura de los pares que no
se pudieron comparar.
relaciones_coleccion( coleccion, pares, muestra = 10000, umbral_cobertura = 0.9, orden = NULL, tope_cache_mb = 512, columnas_candidatas = NULL, podar = FALSE, tope_memoria_mb = 512 )relaciones_coleccion( coleccion, pares, muestra = 10000, umbral_cobertura = 0.9, orden = NULL, tope_cache_mb = 512, columnas_candidatas = NULL, podar = FALSE, tope_memoria_mb = 512 )
coleccion |
Objeto creado por |
pares |
Data frame con columnas |
muestra |
Filas traídas por tabla para comparar, y el mismo tope con
que se calcula cada cobertura: se reenvía a |
umbral_cobertura |
Cobertura mínima para informar una relación. |
orden |
Columnas para |
tope_cache_mb |
Presupuesto de memoria para la caché de lecturas, en
megabytes. Al agotarse se sigue leyendo sin cachear y |
columnas_candidatas |
Lista nombrada por identificador de tabla. Cada
vector declara las columnas que pueden participar; una tabla no nombrada
conserva todas sus columnas. Un nombre se busca con la grafía exacta y,
si no está, sin distinguir mayúsculas —como |
podar |
Si se aplican las podas que cambiarían lo informado —tipos
incompatibles y cardinalidades imposibles—, tal como en
|
tope_memoria_mb |
Presupuesto de memoria para resultados de relaciones,
en megabytes. Al agotarse, las combinaciones pendientes se declaran en
|
Los pares se declaran, igual que la frontera. Con mil tablas hay casi un
millón de pares dirigidos, así que explorar todos no es una opción cara sino
una opción imposible. estimar_costo_coleccion() permite verlo antes.
Cada tabla se lee una sola vez. Los pares repetidos se descartan, los
pares de una tabla consigo misma se rechazan, y las lecturas se guardan en
una caché con presupuesto de memoria declarado: dos pares que comparten una
tabla ya no la leen dos veces. meta$lecturas deja el SQL exacto, la vía
usada, las filas leídas y el momento de cada lectura.
El orden no se supone. Sin orden, una lectura acotada devuelve un
subconjunto arbitrario y el resultado declara meta$estable = FALSE. Declarar
columnas de orden hace la lectura repetible.
Una columna que la sesión no puede escribir no se lleva la tabla. Con
odbc bajo el locale C -un Rscript de cron- un nombre no ASCII no se
puede mandar, y un nombre con bytes que no son UTF-8 válido y no declaran su
codificación -un CSV latin1 leído sin fileEncoding- no se manda con ningún
controlador. Una candidata así sale de la lectura y sus combinaciones quedan
en cobertura_podas con motivo columna_no_escribible y el remedio en
detalle; las demás se comparan. Una columna de orden así deja la lectura
de esa tabla sin ORDER BY —no se puede ordenar como se declaró—, y
meta$lecturas y meta$estable lo dicen. Los nombres publicados son los de
DBI::dbListFields(), los mismos que publica perfilar_coleccion(), aunque
el controlador devuelva los de la lectura en otra codificación.
Una columna que la credencial no puede leer, o que llega enmascarada, no
se compara y no se lleva la tabla. Las columnas de cada tabla y lo que la
credencial puede leer de ellas salen del catálogo del motor, de la misma
función que usa perfilar_dbi(): en PostgreSQL pg_attribute con
has_column_privilege() —dbListFields() de RPostgres no lista las
columnas sin privilegio—, y en SQL Server sys.columns con
HAS_PERMS_BY_NAME() y sys.masked_columns (enmascaramiento dinámico, salvo
que la credencial tenga UNMASK). Sin candidatas declaradas la lectura nombra
las demás columnas en vez de SELECT *, que con un permiso por columna se
rechaza entero, y la columna no se nombra en ninguna consulta. Sus
combinaciones quedan en cobertura_podas con motivo columna_sin_permiso o
columna_enmascarada: una columna enmascarada lee la máscara —xxxx—, y dos
así parecían relacionadas sin compartir un solo valor. Una columna de orden
así deja la lectura sin ORDER BY, como la que no se puede escribir.
Los valores se leen como los lee perfilar_dbi(). RPostgres entrega 0
en toda celda money, y MIN/MAX de una columna money también: la
lectura de cada tabla y la consulta de rangos de la poda la leen convertida
en el motor —CAST(x AS NUMERIC)—, con la misma expresión que
perfilar_dbi() publica en meta$lectura_convertida. Saber el tipo cuesta
una consulta de cero filas por tabla, sólo en las conexiones donde la lectura
convierte algo. Y el texto que el controlador entrega en latin1 con la marca
UTF-8 —odbc con FreeTDS bajo un locale que no es UTF-8, sobre una columna
varchar— se lee como latin1, así que se compara con el de una nvarchar;
la lectura lo declara en meta$lecturas$motivo, con el remedio, porque un
carácter fuera de latin1 llega como ? (ver «Fallo parcial» en
perfilar_dbi()). Con muestra = Inf no se escribe LIMIT: la tabla se
lee entera por la vía sin tope. Dentro de una transacción del usuario, la
lectura sigue la regla de perfilar_dbi(): en PostgreSQL cada sentencia va
dentro de un punto de guardado y la transacción no queda abortada.
Cada par se compara sobre una muestra de filas de cada tabla, y el resultado declara ese alcance: una relación candidata sobre una muestra no es una clave foránea comprobada, es un indicio que hay que confirmar contra el diccionario de datos.
Con la tabla de referencia truncada la cobertura es una cota inferior. Un
valor de tabla_2 que no está entre las filas leídas de tabla_1 puede
estar en las demás, así que la cobertura sólo puede subir al leer más. Una
combinación de columnas que con la referencia truncada no alcanza
umbral_cobertura no se descarta en silencio, aunque otra del mismo par
lo alcance: un indicador de dos valores llega a cobertura 1 trivialmente, y
no puede borrar la clave foránea que quedó en 0,5 por la lectura. El par
queda en cobertura_pares como «sin conclusión» —una fila por par, no una
por combinación—, con cuántas combinaciones no alcanzaron el umbral, las
cinco de mayor cobertura medida con su cota, cuántas lo alcanzaron —que están
en relaciones— y cómo resolverlo. meta$pares_sin_conclusion los cuenta
—también están en pares_comparados, porque se compararon—.
Los motivos de cobertura_pares citan en claro lo que el usuario declaró y
lo que el resto de la salida ya publica: los nombres de las tablas, de sus
esquemas y de sus columnas, y las de orden y columnas_candidatas; el
resto del mensaje del motor sigue la regla de perfilar_dbi().
El alcance se declara por par y en la fila. Cada relación publica
filas_leidas_1/filas_leidas_2 —cuántas se compararon—, y además
filas_totales_1/filas_totales_2 y muestreado_1/muestreado_2: cuando la
lectura fue completa, el total de la tabla es exacto; cuando quedó truncada,
el total va NA y muestreado vale TRUE, porque lo único que se sabe es
que hay más filas que el tope. Publicar el tope como si fuera el total sería
decir que se leyó todo. Para saberlo se pide una fila más que el tope, que
cuesta mucho menos que un COUNT(*) por tabla y contesta exacto; esa fila de
sobra no viaja al resultado.
Antes, lo único que sobrevivía eran los atributos de detectar_relaciones()
del primer par publicado —rbind() conserva los del primer argumento—,
con etiquetas tabla1/tabla2 que no nombran a ningún par: con dos pares
decían muestreado = FALSE mientras el segundo había leído 10.000 filas de
20.000. Esos atributos ya no viajan en relaciones; lo que decían vive en las
columnas, una por par, y el conteo de pares en meta, que cuenta todos.
Las columnas candidatas se podan antes de materializar cada comparación. Las
podas quedan declaradas en cobertura_podas, con su motivo y conteo.
La poda cierta por rangos usa MIN y MAX sobre el universo completo de
cada tabla, no sobre las filas de la muestra. Si el motor no puede entregar
ese rango, la comparación sigue sin aplicar esa poda.
Un entero ancho que llega como doble no se compara. Por encima de 2^53 un
doble no distingue dos enteros consecutivos, y dbConnect(duckdb::duckdb())
entrega BIGINT como doble por omisión: dos identificadores distintos se
fundían y el objeto afirmaba una relación que el motor no tiene. Una columna
leída como doble con algún valor de al menos 2^53 en valor absoluto deja sus
pares en sin_comparar, con motivo entero_como_doble en
cobertura_podas y el remedio en el detalle: conectar pidiendo integer64
—duckdb(bigint = "integer64"), o bigint = "integer64" en RSQLite—.
El campo detalle de una poda por rangos disjuntos no publica extremos que
identifiquen: si el mínimo o el máximo de un lado llega al piso de la
protección —seis caracteres o más—, ese rango sale como [valor protegido].
El mínimo de una columna de documentos es el documento de una persona real, y
el par mínimo–máximo acota además a todos los valores intermedios.
Objeto relaciones_coleccion con relaciones, cobertura_pares,
cobertura_podas y meta.
coleccion(), estimar_costo_coleccion(), detectar_relaciones()
if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:20, nombre = letters[1:20])) DBI::dbWriteTable(con, "visitas", data.frame(persona_id = c(1:15, 1:5))) col <- coleccion(con, c("personas", "visitas"), nombre = "padron") relaciones_coleccion( col, pares = data.frame(tabla_1 = "personas", tabla_2 = "visitas"), columnas_candidatas = list(personas = "id", visitas = "persona_id"), orden = list(personas = "id", visitas = "persona_id") ) DBI::dbDisconnect(con) }if (requireNamespace("RSQLite", quietly = TRUE) && requireNamespace("DBI", quietly = TRUE)) { con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") DBI::dbWriteTable(con, "personas", data.frame(id = 1:20, nombre = letters[1:20])) DBI::dbWriteTable(con, "visitas", data.frame(persona_id = c(1:15, 1:5))) col <- coleccion(con, c("personas", "visitas"), nombre = "padron") relaciones_coleccion( col, pares = data.frame(tabla_1 = "personas", tabla_2 = "visitas"), columnas_candidatas = list(personas = "id", visitas = "persona_id"), orden = list(personas = "id", visitas = "persona_id") ) DBI::dbDisconnect(con) }
Genera un unico archivo HTML en espanol usando solo funciones de R base. El estilo se incluye dentro del documento: no requiere red, navegador especial, conversor externo ni archivos auxiliares. Cada valor dinamico se escapa antes de incorporarlo al documento.
reportar( x, ..., archivo = tempfile("reporte-lupa-", fileext = ".html"), sobrescribir = FALSE, titulo = "Reporte de calidad de datos", fecha = Sys.time(), max_filas = 100L, max_patrones = 20L, proteger_datos_personales = TRUE )reportar( x, ..., archivo = tempfile("reporte-lupa-", fileext = ".html"), sobrescribir = FALSE, titulo = "Reporte de calidad de datos", fecha = Sys.time(), max_filas = 100L, max_patrones = 20L, proteger_datos_personales = TRUE )
x |
Un objeto compatible o una lista de objetos compatibles. |
... |
Objetos adicionales de clase |
archivo |
Ruta de salida. De forma predeterminada crea un archivo en
|
sobrescribir |
Si se permite reemplazar un archivo existente. |
titulo |
Titulo visible del reporte. |
fecha |
Fecha y hora de generacion, inyectable para obtener resultados reproducibles. Se normaliza a UTC. |
max_filas |
Maximo de filas por tabla y de columnas del perfil cuyos patrones se detallan. Las omisiones se informan dentro del reporte. |
max_patrones |
Maximo de patrones mostrados por columna. Las omisiones se informan dentro del reporte. |
proteger_datos_personales |
Si se enmascaran modas, ejemplos, evidencia,
estadisticos de orden, cuantiles y rangos temporales de columnas
cuya clasificacion activa proteccion automatica. Es El enmascarado es el mismo que el de Un plan de limpieza, una medicion, un historico que incluye una medicion
o una comparacion de deriva armados con la proteccion desactivada no se
publican en un informe protegido: su seccion se reemplaza por una que dice
por que se omitio y como pedirla. Lo sabe por una marca que el objeto
lleva, y que |
Se pueden combinar objetos producidos por el profiling, la medicion, la
evaluacion, el historico, las comparaciones de deriva y la planificacion de
limpieza. Cada tipo anade su seccion; el reporte no modifica datos ni aplica
planes. Si una evaluacion contiene desenlaces de supresion declarados por
reglas, el reporte enmascara su valor_medido y el resultado de las mismas
medidas incluidas en el documento, también en las otras evaluaciones del
informe: la supresión de una vale para todas, en cualquier orden, y la de
una medida que un histórico del informe ya trae suprimida, también. El
enmascarado se hace sobre copias y no modifica los objetos recibidos.
Una seccion que no se puede armar se declara dentro del informe y no lo
interrumpe: si a un historico_calidad le faltan campos que su seccion
necesita -fecha, id_medicion, nivel, perfil o resultado-, la seccion
dice cuales faltan y el resto del documento se escribe igual. Lo mismo vale
para cualquier otra seccion que falle al armarse -un perfil al que le falta un
componente-: queda una seccion que lo dice. Un texto con bytes que no son
UTF-8 valido se muestra con esos bytes en hexadecimal, como los muestra la
consola, y no interrumpe el informe. Un carácter de control —que un
navegador no dibuja— se escribe con su código, <U+0001>, como en la
evidencia de los hallazgos. Las cifras se escriben con todos sus digitos significativos, sin el
redondeo de la consola: con las cifras que vuelven al valor del objeto
—quince, o dieciséis o diecisiete cuando hacen falta: 0.1 + 0.2 se escribe
0.30000000000000004— y sin notación científica. Una cifra no se abrevia
como un texto: la que sin exponente pasaría de 240 caracteres
—8.98846567431158e+307, el ausente de Stata leído como número— se escribe
con exponente y las mismas cifras, y una celda con varias se abrevia por
cifras enteras.
La ruta normalizada del archivo, de forma invisible.
perfilar(), medir(), evaluar(), historico_calidad(),
comparar_perfiles(), planificar_limpieza()
perfil <- perfilar(datos_administrativos) archivo <- reportar(perfil) unlink(archivo)perfil <- perfilar(datos_administrativos) archivo <- reportar(perfil) unlink(archivo)
Devuelve el catálogo de paquetes R, bibliotecas del sistema, dialectos y
estado de prueba que lupa conoce. La presencia del paquete R se comprueba
en la máquina actual. La biblioteca del sistema no se declara instalada sin
una comprobación específica: cuando corresponde, la columna lo deja como
no_comprobada y conserva las rutas de resolución posibles.
requisitos_motor(motor = NULL)requisitos_motor(motor = NULL)
motor |
|
El estado probado se copia de la tabla de motores de los README. esperado
significa que el dialecto está implementado pero no se comprobó contra ese
motor real en este repositorio. Los motores que no figuran en esa tabla no se
presentan como probados.
Ante una conexión abierta, coleccion() y perfilar_coleccion() piden el
paquete del controlador con que se abrió, que reconocen por la clase de
la conexión y el paquete que la define, no por el motor que informa el
servidor: un MySQL abierto con RMariaDB::MariaDB() usa RMariaDB y no pide
ni carga RMySQL, cuya clase se llama igual y rompería la conexión.
Un data.frame de clase requisitos_motor, con una fila por variante
documentada. Incluye el estado comprobado del paquete R y un estado explícito
para la biblioteca del sistema: no_comprobada no significa instalada.
# El catálogo completo: una fila por variante documentada. todos <- requisitos_motor() nrow(todos) # Lo que hace falta para un motor en particular, antes de intentar conectarse. requisitos_motor("postgresql")[, c("paquete_r", "estado_paquete_r")] # `no_comprobada` en la biblioteca del sistema no significa instalada: el # paquete no puede saberlo sin intentarlo, y lo dice en vez de suponerlo. unique(requisitos_motor()$estado_biblioteca_sistema)# El catálogo completo: una fila por variante documentada. todos <- requisitos_motor() nrow(todos) # Lo que hace falta para un motor en particular, antes de intentar conectarse. requisitos_motor("postgresql")[, c("paquete_r", "estado_paquete_r")] # `no_comprobada` en la biblioteca del sistema no significa instalada: el # paquete no puede saberlo sin intentarlo, y lo dice en vez de suponerlo. unique(requisitos_motor()$estado_biblioteca_sistema)
Declara que varias columnas de la misma tabla codifican el mismo hecho,
de modo que detectar_discordancias() pueda informar las filas donde no
concuerdan. El grupo lo declara quien conoce los datos: lupa no lo adivina,
porque dos columnas de año pueden ser el año de nacimiento y el año de
ingreso y no tienen por qué coincidir.
senal_redundante(columnas, ventana = 0, transformacion = NULL, nombre = NULL)senal_redundante(columnas, ventana = 0, transformacion = NULL, nombre = NULL)
columnas |
Nombres de al menos dos columnas que codifican el mismo hecho. |
ventana |
Tolerancia máxima admitida entre los valores, en las unidades
del valor comparado. Por omisión |
transformacion |
Lista opcional con nombres de columna y funciones que las llevan a una escala comparable. |
nombre |
Etiqueta de la señal. Por omisión, las columnas unidas. |
transformacion permite comparar columnas que guardan el hecho de formas
distintas —por ejemplo extraer el año de una fecha para compararlo con una
columna de año—. Cada función recibe la columna entera y devuelve un vector
de la misma longitud.
ventana es la tolerancia, en las unidades del valor transformado: con
años, ventana = 1 acepta un año de diferencia. La unidad no se adivina; es
la del resultado de la transformación.
La ventana sólo gobierna una comparación numérica. Si alguna de las
columnas comparadas no es numérica —fechas, texto, factores—, la comparación es
de igualdad exacta sobre el texto y la ventana no se aplica: eso queda dicho en
la evidencia (comparacion textual exacta) y, si se declaró una ventana mayor
que cero, se avisa al medir. Para comparar fechas con tolerancia hay que
llevarlas con transformacion a un número en la unidad de la ventana.
Esa igualdad exacta es de valores, no de su impresión. Una fecha-hora se
compara por su instante: el mismo instante guardado en dos zonas horarias
concuerda. Se escribe en UTC, al segundo y con su fracción exacta si la tiene
—la misma escritura que usa detectar_relaciones()—, así que comparada con
una columna de texto, el texto tiene que estar en UTC; para otra zona, se la
lleva con transformacion. Un número se compara con todas sus cifras —1e5
y el texto "100000" concuerdan; 0.1 + 0.2 y 0.3 no—. Si todas las
columnas son duraciones (difftime), se comparan en segundos: una hora y
sesenta minutos concuerdan. Un texto se compara por sus bytes, así que una
palabra con la tilde compuesta y la misma con la tilde descompuesta son dos
textos distintos.
Objeto de clase senal_redundante: una lista con nombre -el
declarado, o las columnas unidas por =-, el vector columnas que la
señal relaciona, la ventana numérica y la transformacion validada.
Describe la relación esperada; no la evalúa.
detectar_discordancias(), detectar_dependencias()
senal_redundante(c("anio_fiscal", "anio_archivo")) senal_redundante( c("fecha", "anio_fiscal"), transformacion = list(fecha = function(x) as.integer(format(x, "%Y"))) )senal_redundante(c("anio_fiscal", "anio_archivo")) senal_redundante( c("fecha", "anio_fiscal"), transformacion = list(fecha = function(x) as.integer(format(x, "%Y"))) )
Vector para solicitar explícitamente la lista numérica completa publicada
por naniar. Sus autores incluyen a
Nicholas Tierney, Di Cook,
Miles McBain y Colin Fay.
Incluye 66, 77,
88 y 9999, que pueden ser edades,
códigos o años legítimos y por eso no se aplican de forma predeterminada.
Se usa como perfilar(datos, sentinelas_numericos = sentinelas_naniar).
Tanto este vector como las cadenas de ausencia incorporadas en el paquete
están congelados con referencia a naniar
1.1.0; no cambian según la versión instalada.
sentinelas_naniarsentinelas_naniar
Vector numérico de ocho elementos.
naniar::common_na_numbers, versión 1.1.0. Véase el
repositorio de naniar.
perfilar(), planificar_limpieza()
sentinelas_naniar datos <- data.frame(codigo = c(1, 66, 9999)) perfil <- perfilar( datos, sentinelas_numericos = sentinelas_naniar, analizar_dependencias = FALSE ) perfil$columnas[, c("columna", "n_faltantes_disfrazados")]sentinelas_naniar datos <- data.frame(codigo = c(1, 66, 9999)) perfil <- perfilar( datos, sentinelas_numericos = sentinelas_naniar, analizar_dependencias = FALSE ) perfil$columnas[, c("columna", "n_faltantes_disfrazados")]
Ordena las columnas que podrían identificar una fila, para que quien conoce la tabla elija. No decide: una columna única puede ser una clave o puede ser una magnitud que no repite, y esa diferencia no está en los datos.
sugerir_clave(datos, maximo = 5L, umbral_casi = 0.95)sugerir_clave(datos, maximo = 5L, umbral_casi = 0.95)
datos |
Tabla a examinar. |
maximo |
Cuántas sugerencias devolver como máximo: un entero positivo. |
umbral_casi |
Proporción de valores distintos a partir de la cual una columna que repite se informa igual, como candidata con duplicados. |
El orden combina tres señales, y las tres se publican para que se pueda
discutir el orden en vez de aceptarlo: si la columna identifica cada fila
sin repetir, si no tiene ausentes —una clave con ausentes no identifica—,
y cuánto se parece su nombre al de una clave (id_persona, documento,
expediente). El nombre se compara sin acentos ni separadores, así que
ID_Persona e idpersona pesan igual.
Las columnas que repiten valores no se ofrecen como clave de una sola columna, pero sí se informan cuando estuvieron cerca: una columna que identifica al 99 % suele ser una clave con duplicados de carga, que es exactamente lo que conviene mirar.
Una columna double con valores de parte fraccionaria —un importe, una
coordenada— puede identificar cada fila y aun así no ser una clave. No se
oculta que identifica, porque es un hecho medido: se dice en el motivo y la
columna queda al final del orden. Es el mismo criterio que aplica
detectar_claves(), consultado a la misma función.
Las columnas compuestas —matrices o arreglos de más de una dimensión— no se
ofrecen como claves ni reciben una tasa de valores distintos.
Una columna cuyo nombre se repite en la tabla no se mide: la clave se pasa a
perfilar() por nombre, y ese nombre no dice cuál de las dos es. Aparece al
final, fuera de maximo, con identifica, sin_faltantes y
tasa_distintos en NA y el motivo; elegir_clave() no la ofrece.
Un data.frame con una fila por columna candidata, ordenado de más a
menos probable, con las tres señales por separado y el motivo en texto.
Cero filas si ninguna columna se acerca.
detectar_claves() para las combinaciones de varias columnas, y el
argumento clave de perfilar() para declarar la que se elija.
personas <- data.frame( id_persona = 1:4, documento = c(11111111, 22222222, 33333333, 33333333), edad = c(30, 41, 25, 25) ) sugerir_clave(personas)personas <- data.frame( id_persona = 1:4, documento = c(11111111, 22222222, 33333333, 33333333), edad = c(30, 41, 25, 25) ) sugerir_clave(personas)
Resume una corrida en una fila por métrica y objeto. Las métricas
booleanas usan ratio por omisión y las reales usan promedio; la
elección queda siempre en la columna agregacion. ratio_umbral sólo se
aplica cuando se declara también el umbral correspondiente.
tablero_calidad( medidas, agregaciones = NULL, umbrales = NULL, marco = NULL, cobertura = NULL )tablero_calidad( medidas, agregaciones = NULL, umbrales = NULL, marco = NULL, cobertura = NULL )
medidas |
|
agregaciones |
|
umbrales |
Vector numérico opcional con nombres de métrica
instanciada. Se exige para cada |
marco |
Marco conceptual. Si se omite, usa el asociado a la medición,
el marco AGESIC cuando corresponde o el conjunto de factores medidos —los
tres caminos contienen lo que se publica—. Un marco declarado puede no
contener los pares medidos: no se rechaza, porque informar contra la
taxonomía propia es un uso previsto, pero lo que queda afuera se cuenta en
|
cobertura |
Cobertura opcional creada por La correspondencia entre esa cobertura y |
La identidad de cada celda son tres columnas: metrica_instanciada,
entidad y objeto. No alcanza con metrica y objeto, porque metrica
es el nombre genérico: dos tablas con una columna del mismo nombre, o dos
especializaciones de la misma genérica sobre la misma columna, publicaban dos
filas con la misma identidad y valores distintos, y rehacer la celda con
las claves publicadas mezclaba las medidas de las dos sin reproducir ninguna.
metrica_instanciada sola tampoco alcanza: sobre un agregado vale
agregada:<funcion>:<metrica> para todas las filas, y ahí la entidad es lo
único que separa. Cuando hay varias entidades, objeto además nombra la
tabla —cod (tabla: t1)—, en el mismo idioma con el que la granularidad de
tabla ya publicaba (tabla: t1).
Ese trío es la clave con la que se agrupan las medidas, y no sólo la que se
publica: agrupar por menos de lo que se declara como identidad funde dos cosas
que el objeto dice distintas. Antes, las granularidades sin rama propia
—conjuntoAtributos entre ellas— agrupaban sólo por el objeto, y dos entidades
con el mismo objeto salían como una celda con la entidad de la primera y el
promedio de las dos.
Y la unicidad se verifica: si dos celdas quedaran con el mismo trío, la función falla nombrando las dos y sus valores, en lugar de publicar dos filas con la misma identidad. Se rechaza en vez de quedarse con una porque dos valores distintos para la misma identidad es una contradicción de la entrada, y elegir uno sería elegir por quien llama.
El objeto conserva la cobertura completa del marco: factores medidos, sin
métrica declarada, no aplicables y fuera de alcance. print() muestra
ambos elementos para que unas pocas filas nunca se lean como cobertura total.
Esa cobertura se cuenta sobre los factores del marco, así que una medida
cuyo par dimensión-factor el marco no declara no entra en ninguna de sus
casillas. Eso es legítimo —declarar el marco propio y medir con las métricas
de lupa es un flujo previsto, y la cobertura de ese marco puede satisfacerse
por perfil_mide—, pero leer «se midieron 0 factores de este marco» al lado
de una fila con valor parece una contradicción del objeto. Por eso el alcance
publica además medidos_fuera_del_marco, que no suma con las otras cuatro
casillas, y el atributo pares_fuera_del_marco los nombra.
Data frame S3 tablero_calidad. Los atributos alcance y
cobertura conservan los conteos y el detalle del marco, y
pares_fuera_del_marco nombra los pares medidos que ese marco no declara.
medir(), agregar(), indice_calidad()
nucleo <- metricas_nucleo() instancia <- instanciar(especializar(nucleo$NoNulo), "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) tablero_calidad(medidas)nucleo <- metricas_nucleo() instancia <- instanciar(especializar(nucleo$NoNulo), "personas", "edad") medidas <- medir(modelo(instancia), data.frame(edad = c(20, NA, 35))) tablero_calidad(medidas)
Estas funciones son puras y vectorizadas: no consultan la red ni prueban la
existencia de un país, moneda, buzón o entidad emisora. Los códigos se
contrastan con copias locales de las listas vigentes al preparar esta
versión del paquete. Los valores ausentes devuelven NA; todo valor presente
que no cumple el contrato devuelve FALSE. Los espacios al borde se
recortan antes de validar —son ruido de captura, no parte del dato—, salvo en
validar_url(), donde un espacio literal en cualquier posición rompe la
sintaxis y el valor es inválido. Se recorta todo espacio: el ASCII, el
tabulador y los separadores de Unicode, como el espacio duro (U+00A0) que
dejan las planillas y las páginas web. Un espacio en el medio no se recorta
en ningún validador.
validar_url(x, esquema_obligatorio = TRUE) validar_iso3166(x, tipo = c("alpha2", "alpha3", "numerico")) validar_iso4217(x) validar_correo(x) validar_luhn(x) validar_mod97(x)validar_url(x, esquema_obligatorio = TRUE) validar_iso3166(x, tipo = c("alpha2", "alpha3", "numerico")) validar_iso4217(x) validar_correo(x) validar_luhn(x) validar_mod97(x)
x |
Vector que se desea validar. |
esquema_obligatorio |
Si es |
tipo |
Forma de código ISO 3166: dos letras ( |
La respuesta es sobre el dato, no sobre cómo lo escribe R. Un número se valida
por sus dígitos: el doble 12000000 se lee como "12000000" aunque scipen
lo imprima en notación científica. Un doble desde 2^53 no conserva sus
dígitos —es el más cercano de varios enteros— y devuelve FALSE; el mismo
número como texto o como integer64 sí se valida. Una magnitud con clase
—Date, POSIXct, difftime, hms, units— no es la escritura de un
documento y devuelve FALSE (o NA donde falta), sin validar los días o
segundos que guarda. NaN es ausente y devuelve NA, igual que
en el resto del paquete. Las letras se pasan a mayúsculas sólo dentro de ASCII:
toupper() convierte la s larga (U+017F) en S y la i sin punto (U+0131) en
I, y con eso una s larga seguida de e pasaba por un código ISO; ahora es
inválida. Y la marca de codificación del texto
—latin1 o UTF-8— no cambia la respuesta.
validar_correo() comprueba un subconjunto práctico y deliberadamente
conservador de la sintaxis addr-spec: parte local de puntos y caracteres
ASCII habituales, seguida por un dominio DNS con al menos un punto. No
admite comentarios, cadenas entre comillas ni literales de dominio válidos
en la gramática completa de RFC 5322, y no prueba entrega ni existencia.
validar_url() comprueba URLs jerárquicas con host y acepta sólo los
esquemas http y https: son los esquemas de red que el paquete puede
reconocer sin atribuir semántica a protocolos arbitrarios. javascript: y
data: se rechazan deliberadamente, incluso si se parecen a una cadena con
dos puntos. Por omisión el esquema es obligatorio; con
esquema_obligatorio = FALSE también se acepta un host como ejemplo.uy.
Se aceptan nombres de dominio Unicode (IDN) y nombres ASCII en punycode,
puertos entre 1 y 65535, rutas, consultas y fragmentos con codificación
porcentual válida. No se consultan DNS ni se afirma que el recurso exista.
Los espacios, separadores Unicode y caracteres de control literales hacen
que el valor sea inválido; un espacio porcentualmente codificado sigue la
sintaxis de una URL y no es un espacio literal.
validar_luhn() acepta únicamente dígitos y aplica el algoritmo de Luhn.
validar_mod97() acepta letras ASCII y dígitos, transforma las letras a
A = 10, ..., Z = 35 y exige resto 1 conforme a ISO 7064 MOD 97-10. No
reordena caracteres: protocolos como IBAN deben hacer antes su transformación
propia.
Vector lógico de la misma longitud que x.
International Organization for Standardization. ISO 3166 Country Codes. https://www.iso.org/iso-3166-country-codes.html
SIX Group. ISO 4217 Currency Codes. https://www.six-group.com/en/products-services/financial-information/market-reference-data/data-standards.html
Resnick P (2008). Internet Message Format, RFC 5322. https://www.rfc-editor.org/rfc/rfc5322
Luhn HP (1960). Computer for Verifying Numbers, US Patent 2,950,048. https://patents.google.com/patent/US2950048A/en
International Organization for Standardization. ISO/IEC 7064:2003. https://www.iso.org/standard/31531.html
pack_validadores(), validar_ci_uy(), especializar()
validar_iso3166(c("UY", "CL", "ZZ")) validar_iso4217(c("UYU", "CLP", "ZZZ")) validar_correo(c("[email protected]", "sin-arroba")) validar_url(c("https://ejemplo.uy", "ejemplo.uy")) validar_url("ejemplo.uy", esquema_obligatorio = FALSE) validar_luhn(c("79927398713", "79927398714")) validar_mod97(c("9999123456789012141490", "9999123456789012141491"))validar_iso3166(c("UY", "CL", "ZZ")) validar_iso4217(c("UYU", "CLP", "ZZZ")) validar_correo(c("[email protected]", "sin-arroba")) validar_url(c("https://ejemplo.uy", "ejemplo.uy")) validar_url("ejemplo.uy", esquema_obligatorio = FALSE) validar_luhn(c("79927398713", "79927398714")) validar_mod97(c("9999123456789012141490", "9999123456789012141491"))
validar_ci_uy() comprueba la cédula de identidad mediante su dígito
verificador módulo 10. Acepta siete u ocho dígitos y separadores de puntos,
espacios o guion; una cédula de siete dígitos se completa con cero inicial.
Cualquier espacio separa, también el fino o el duro de Unicode.
validar_ci_uy(x) validar_rut_uy(x)validar_ci_uy(x) validar_rut_uy(x)
x |
Vector que se desea validar. |
validar_rut_uy() comprueba la estructura de doce dígitos del RUT y su
dígito verificador módulo 11. Un RUT de once dígitos se completa con cero
inicial, igual que la cédula de siete: es el que pierde su cero al guardarse
como número (read.csv() lee 073602060015 como 73602060015). El algoritmo operativo no está publicado por
DGI como especificación normativa abierta; esta implementación se contrastó
con la implementación pública de python-stdnum y con ejemplos públicos.
Por eso valida estructura y dígito, no vigencia ni existencia registral.
Los separadores son de la escritura, no del número. Punto, espacio y
guion se admiten porque una persona escribe 1.234.567-2; en un valor
numérico el punto es el separador decimal y el guion es el signo, así que un
número fraccionario o negativo devuelve FALSE —no se le quita la fracción
ni el signo para hacerlo pasar—. Un número entero no negativo sí se valida,
como cualquier otra escritura del mismo documento, y por sus dígitos: el
doble 12000000 es la cédula "12000000" aunque scipen lo imprima en
notación científica. NaN es ausente y devuelve NA.
Vector lógico de la misma longitud que x.
Poder Ejecutivo de Uruguay (1978). Decreto 501/978, artículo 2. https://www.impo.com.uy/bases/decretos/501-1978/2
Unidad de Acceso a la Información Pública (2024). Resolución 145/024. https://www.gub.uy/unidad-acceso-informacion-publica/institucional/normativa/resolucion-n-145024-sobre-reserva-informacion
de Jong A. python-stdnum: Uruguay RUT. https://github.com/arthurdejong/python-stdnum/blob/master/stdnum/uy/rut.py
pack_validadores(), validar_iso3166()
validar_ci_uy(c("1.234.567-2", "1.234.567-3")) validar_rut_uy(c("21 100 342 0017", "21 030 367 0014"))validar_ci_uy(c("1.234.567-2", "1.234.567-3")) validar_rut_uy(c("21 100 342 0017", "21 030 367 0014"))