lupa 0.1.0
Ronda 23: el tablero, el indice y la proteccion otra vez
Una refutacion de la agregacion, el tablero y el indice encontro doce defectos, y la suite uno mas que venia de antes; una de la proteccion, nueve fugas y tres familias de tapado de mas en lo que la ronda 22 habia aflojado.
El tablero y el indice:
- La supresion del tablero se empareja por la metrica instanciada: con una especializacion con nombre propio, el tablero, el indice y el informe publicaban el valor que la regla mando suprimir; sin nombre, se tapaba tambien la instancia que cumplia.
-
El alcance agregado suma las partes completas -“11 de 12” y no “5 de 6”- y, en un destino por fila, declara solo las filas incompletas, con su numero.
rbind()de mediciones conservaalcance_medidas. -
ratio_umbralalcanza el umbral en el borde -1 - 0.9llega a0.1-, con la misma cuenta en el tablero y enagregar(); el universo de una celda no depende del camino; el promedio de una duracion conserva su tipo y no pasa por una proporcion. -
Los identificadores de medida dejan de numerarse por llamada:
medir(),agregar(),analizar()y el tablero numeraban desde 1 en cada una, y dos partes de la misma corrida -dos colecciones agregadas por separado, o dos tablas medidas con el mismoid_mediciony unidas conrbind()- salian con los mismosid_medida:evaluar()yhistorico_calidad()las rechazaban como una medicion unida consigo misma, en el camino documentado para subir de nivel. Ahoramedir()antepone la metrica instanciada y numera dentro de ella (M-NoNulo@t.x-000001), yagregar()nombra destino, funcion, metrica y objeto (M-agg-atributo-ratio-NoNulo@t.x). -
agregar()y el tablero rechazan una medida repetida, y un valor que no se puede agregar se rechaza nombrando la metrica y la causa; unumbralque nadie usa se rechaza; una metrica sin dimension se nombra en el tablero y se excluye del indice con su motivo; los mensajes de pesos dicen cuando una dimension se excluyo o cuando un nombre difiere solo en su forma Unicode.
La proteccion:
- Un telefono guardado con forma de fecha no sale del piso: el piso aceptaba cualquier ano. El rango de anos plausibles -1800 a 2100- es uno solo para el piso y la regla de digitos.
- Un nombre de columna dentro de un valor protegido no lo exceptua.
- Un numero con forma de IP se reconoce como documento entero, y un decimal solo es inequivoco si todo el numero son decimales: el telefono “29.10.12.34” o “(02) 901.1234” se publicaba.
- La fecha con hora se reconoce con coma, con dia y mes en los dos ordenes, con la hora separada por puntos y en la forma compacta; la fraccion de segundo tiene tope; mas espacios, invisibles y parecidos de Unicode unen el numero; y el motivo de
perfilar_dbi()tapa una cita con sus comillas de CSV. -
Tapa menos de mas: los dos puntos y la barra vertical ya no unen numeros, el numeral y la vineta ya no son comodines, una IP al final de una frase y el ISBN-10 ya no se toman por documentos.
?perfilarlista lo que queda.
Ronda 22: el modelo de calidad
Una refutacion de medir(), evaluar(), el historico y la deriva encontro doce defectos.
-
Con
aplicabilidad, toda metrica cita la fila de la tabla original: ocho de las once del paquete publicaban la fila del recorte -t$x[3]donde habian medido la 6-, y el plan de desenlaces mandaba suprimir otra celda. Una metrica de varias columnas se mide donde corresponden todas, y ya no depende del orden de sus atributos. -
[,subset()yrbind()conservan lo que la medicion y el historico declaran: la ronda 21 conservaba solo la marca de privacidad, y se perdian el modelo, la cobertura y la configuracion de cada corrida -evaluar()publicaba como exito una regla sin medidas segun el orden derbind(), y la deriva leia un cambio de modelo como deterioro-.rbind()se niega a unir mediciones de modelos distintos, y el de dos historicos los acumula. - La supresion declarada sobrevive a acumular la medicion despues de la evaluacion, tambien sobre un historico guardado o leido de un CSV.
-
AtributoDuplicadomarca todas las apariciones tambien eninteger64, y los conjuntos declarados -valores_nulos,valores,diccionario- y la regla de integridad entre entidades emparejan los numeros por su valor:1e5y elinteger64100000 coinciden. -
ErrorEstandarse declara no medible ante un valor infinito, comoEscala, en vez de publicar la dispersion del resto. -
evaluar()mira la cobertura de las reglas corrida por corrida, rechaza una medida repetida o conresultadoNAnombrandola, y el resumen por regla no funde nombres que se pegan con un punto. - Una fecha de texto se lee en UTC, en
medir()y en el historico: un historico exportado conwrite.csv()y releido conserva sus horas, y su deriva ya no ordena al reves dos corridas del mismo dia. - Una misma corrida armada por partes -metricas medidas por separado con el mismo
id_medicion- se une conrbind(), y su modelo es la union de los de las partes.
Y una refutacion de la proteccion de datos personales encontro ocho fugas y una familia de tapado de mas en la regla de digitos de la ronda 21:
- Los guiones, espacios e invisibles de Unicode no cortan el numero: un telefono con un guion largo, un espacio de cifra o un espacio de ancho cero entre sus grupos se publicaba.
-
Un numero con puntos no es un importe si no tiene forma de miles:
099.12.34.56,01.23.45.67.89oCI 4.123.456.7se publicaban; el mismo signo no separa miles y decimales a la vez. - Una fecha solo se borra antes de buscar si su ano es plausible -el telefono
2901-12-12no es una fecha-, y una fecha con hora protegida se reconoce escrita en otro formato. - En la prosa, una cita con digitos de otra escritura se prueba; un nombre de columna con tildes no exceptua al valor protegido que es; y el desvio se tapa junto con el extremo que el piso tapo, porque lo reconstruia.
-
perfilar_dbi()lee como texto, en SQLite, una columna de afinidad numerica que guarda documentos, y el motivo de una cifra que no se pudo leer no cita el valor de una columna protegida. -
Tapa menos de mas: una coordenada de seis decimales, un par de coordenadas, una hora con fraccion de segundo, una direccion IP, un ISBN y dos telefonos separados por una barra ya no se toman por documentos. Lo que queda declarado en
?perfilar, con su reproductor ampliado.
La cuarta evaluacion real
-
Un nombre de columna no se tapa aunque contenga un valor protegido. Con “Segundo” -un nombre de pila corriente- protegido, la columna
segundo_nombresalia[valor protegido]_nombreen los hallazgos sin accion del plan, encolumnas_analizadasy en la SQL guardada deperfilar_dbi(): el hallazgo declarado dejaba de corresponder a su columna y la SQL nombraba una que no existe. Los nombres son estructura en todas las salidas, tambien cuando un texto los escribe entre comillas; salvo el que es, el mismo, un valor protegido.
Ronda 21: el informe HTML
Una refutacion del informe de reportar() encontro siete defectos.
-
Un plan, una deriva, una medicion o un historico armados con la proteccion desactivada no se publican en un informe protegido. Salian en claro al lado de un perfil enmascarado del mismo archivo; ahora su seccion dice que se omitio y como pedirla. La marca que lo indica sobrevive a
[,subset()yrbind()-que la perdian o se quedaban con la del primer objeto- y al historico. - En la seccion Historico, una medida que la evaluacion suprime se enmascara como en la evaluacion del mismo informe.
- Un perfil, o un analisis sin la tabla, armado con la proteccion desactivada se vuelve a proteger con lo que conserva, y su seccion declara que una variante de un valor protegido escrita en otra columna puede quedar a la vista.
- El informe no aborta con texto marcado UTF-8 que no es UTF-8 valido -el que deja
fread(encoding = "UTF-8")sobre un CSV latin1-: lo muestra con sus bytes. - Las cifras se escriben con todos sus digitos: el informe redondeaba a ocho cifras significativas y publicaba numeros que no estaban en el objeto.
- Una seccion que no se puede armar -un perfil al que le falta un componente- se declara dentro del informe en vez de interrumpirlo; y un salto de linea en
tituloya no se escribe como<br>dentro de<title>.
Y una refutacion de la proteccion de datos personales encontro catorce, cinco de ellos abiertos por lo que la ronda 20 aflojo.
- Un numero protegido se reconoce con cualquier agrupacion: un telefono “2901 1234” o “099 12 34 56”, una tarjeta, un IBAN, la cedula escrita “4 . 123 . 456-7” o con puntos medios se publicaban enteros, porque la regla de la ronda 20 solo aceptaba la forma de miles. Tambien con ceros a la izquierda, redondo -la cedula 5.000.000-0- y con digitos arabigo-indicos, persas o devanagari.
- El documento sin su primer digito se reconoce tras un comodin -“*.012.345-8”-, tambien cuando el segundo digito es cero.
-
Tapa menos de mas: la parte decimal de una coordenada o de un p-valor, el entero de un monto con decimales, una hora o una lista separada por comas ya no se toman por documentos. Lo que queda tiene un costo medido y declarado en
?perfilar: un entero de siete cifras que coincide con un documento sin su verificador no se distingue de el.data-raw/medir_tapado_de_mas.Rrehace la cifra. - En la prosa se tapa el numero o la cita, no la frase, y el umbral de una sugerencia -escrito sin comillas- ya no publica el documento que es. Una comilla en el nombre de una columna no desarma el apareo de las citas.
-
Los campos numericos comparan tambien la forma en digitos del documento escrito -“5.432.198-6” frente a un
maximode 54321986-, y cuando el piso tapa un extremo se tapan los campos de la secuencia entera que lo reconstruian. - Una columna de p-valores de siete decimales ya no se clasifica como documento.
-
perfilar_dbi()trae los valores de las columnas protegidas de la tabla entera cuando la muestra no la cubre, tambien de una columna numerica, y protege con la misma regla queperfilar(). Antes comparaba exacto, no miraba la evidencia y se cortaba sin avisar en 500 candidatos. Si no puede traerlos, tapa y lo declara.
Ronda 20: el plan de limpieza, y la proteccion otra vez
Dos refutaciones en paralelo. La de la capa de limpieza encontro veintidos defectos -no se atacaba desde la ronda 9-; la de privacidad, nueve, tres de ellos abiertos por la ronda 19.
El plan de limpieza:
-
convertir_titulopone la mayuscula en la inicial aunque este acentuada -Penacon enie saliaPenA- y capitaliza fuera del latin; las tres acciones de caja usan el mapa del paquete y no dependen del locale. -
winsorizar_outliersconserva la clase de la columna: sobre texto con numeros la pasaba entera a numero y borraba los ceros iniciales que el propio plan protege;InfyNaNquedan como estaban; una fecha vuelve como fecha. -
Con
aplicabilidad, las acciones de outliers miden y tratan solo el universo: marcaban filas de afuera y recortaban con los limites de la columna entera. El registro cuenta las irreversibles. -
Una lista
valoresvacia enconvertir_ausencias_textualesno convierte nada -convertia todo lo detectado- y el registro dice por que. -
guiar_limpieza()no muestra valores de columnas protegidas como ejemplos. -
Las acciones de duplicados usan la igualdad del perfil:
0.1 + 0.2no es0.3, latin1 no es UTF-8, yconservar_mas_completano junta las claves ausentes ni borra una fila con clave distinta. -
normalizar_nombresaplica losnombres_propuestoseditados y no renombra las marcas que agrega el plan; sobre unsfactualiza su geometria. -
El plan no tapa el catalogo de centinelas ni el vocabulario compartido de columnas no personales, y con
proteger_datos_personales = FALSEno protege. - Las acciones de texto conservan los atributos de la columna -la etiqueta de
haven-; las referencias HTML entre 128 y 159 se leen como Windows-1252; unordende texto se rechaza; activar a mano una reparacion de codificacion parcial la aplica; una accion recomendada ejecutada no figurapendiente; el registro de eliminar una columna duplicada nombra la eliminada; y el resumen impreso ya no llama “celdas” a la suma de lo que conto cada accion.
La proteccion de datos personales:
- Un valor con una barra literal, o con una marca del paquete adentro, no se publica. El desescapado de la ronda 19 se aplicaba tambien al valor protegido; ahora solo al texto publicado, que se compara en sus dos formas.
- La regla de digitos parte cada numero por sus separadores -tambien coma, espacio duro, apostrofo y guion bajo- y busca el documento, sin su verificador o sin su primer digito, entre sus tramos: reconoce el verificador mal tipeado o el documento seguido de un ano, y no pega dos conteos vecinos.
-
perfilar_dbi()consulta la base por los valores que va a publicar de las columnas no personales cuando la muestra no cubre la tabla: el titular que solo estaba fuera de la muestra se publicaba. - Un texto que no es UTF-8 valido se compara leido de las dos maneras; una fecha compacta (
AAAAMMDD) o en ISO a medianoche no entra en el piso; y launidadde una columna es vocabulario. - Con un millon de cedulas protegidas, la regla de digitos ya no tapa conteos. En la prosa del paquete solo mira lo citado -sus numeros son conteos-; una fecha, un numero redondo o la parte decimal de otro no son documentos; los tramos que unen grupos tienen que tener forma de numero con separadores de miles; y el documento parcial conserva siete cifras. Medido: de 18, 16 y 6 celdas tapadas de mas a ninguna.
-
comparar_perfiles()declarano_comparableuna politica de centinelas tapada EN PARTE, no solo la tapada entera: ahora la proteccion puede tapar un centinela declarado y dejar el catalogo del paquete a la vista.
Ronda 19: la proteccion de datos personales, por las dos caras
Diez hallazgos de una refutacion con dos encargos: que un valor protegido se publique, y que la proteccion tape lo que no es un valor de nadie.
-
Un valor protegido con un caracter que el paquete escapa no se publica escapado. Un nombre con un espacio duro, un salto de linea o un espacio de ancho cero salia como
Juan<U+00A0>Perezen las evidencias de otra columna: el barrido comparaba el valor crudo con el texto ya escapado. Ahora deshace los escapes antes de comparar, tambien el<lupa-byte:...>con que se citaba un nivel que no es UTF-8. -
perfilar_por()barre su salida con los valores protegidos de la tabla entera. Cada grupo se perfila sobre su rebanada, y el titular que en la columna protegida estaba solo en otro grupo se publicaba exacto. Las etiquetas de grupo se siguen publicando, como esta documentado. -
perfilar_dbi()tapa en el resumen el valor de una persona que es moda o extremo de otra columna. Buscaba los valores enperfil_muestra$datos, que no existe:$hacia coincidencia parcial condatos_personalesy no salia ninguno. Ahora se cosechan de la muestra al leerla. -
El pliegue con que se comparan las variantes cubre la caja de todo alfabeto y la compatibilidad de Unicode: ligaduras, ancho completo y medio ancho, letras matematicas, mayusculas del latin extendido y el georgiano. Sale de un mapa generado (
data-raw/mapa_pliegue_comparar.R), que reemplaza al de la ronda 18-B. - Un valor en CP1252, o una celda con un solo byte roto, se reconoce. Lo que no es UTF-8 valido conserva sus secuencias validas y cada byte suelto se lee como CP1252, que es lo que escribe Windows: leer la celda entera como latin1 convertia en controles las letras de apellidos checos y polacos, y rompia las letras validas de una celda cortada.
-
Las palabras y las marcas del propio paquete no se tapan. Con columnas de nombre y de apellido separadas,
Blancotapaba toda evidencia con la marca<blanco>,Maximola que llevaba la clavegrupo_maximo, yPatronoConstantedescripciones enteras. El lexico del paquete se calcula al instalar, desde las cadenas de su propio codigo: sus marcas no se comparan en ningun campo, y sus palabras no se tapan en su prosa salvo dentro de una cita. - La regla de digitos busca el documento sin su verificador, no cualquier tramo de seis cifras. Con un millon de cedulas protegidas tapaba las evidencias que citan conteos de filas, y pegaba dos conteos vecinos en un solo numero.
- Una fecha de calendario sola no entra en el piso de la proteccion. Con la fecha de nacimiento protegida, la media, la mediana y los extremos de las demas columnas de fechas coincidian con el cumpleanos de alguien y salian tapados. La columna protegida se sigue protegiendo entera.
Un metodo propio que mide fuera de su universo
-
medir()no publica las medidas de un metodo propio que caen fuera del universo que su metrica declara conaplicable. Se publicaban, entraban en el agregado yalcance_medidasdecia «midio 4 de 5 en el universo aplicable» cuando adentro se habian medido 3. Es una salida que no cumple el contrato demetodo, como unafilaque no existe: la metrica quedano_medible, con aviso y con las filas de afuera citadas en el motivo. - Un metodo que no devuelve ninguna medida sobre un universo con valores deja la metrica
no_medible, con aviso. Se publicabasin_valorescon «aportar valores no nulos», que?medirdefine como un universo sin valores.
Ronda 18-B: la proteccion de datos personales, refutada otra vez
-
La sugerencia de una ausencia estructural no publica el criterio de una columna protegida. Si la columna que falta por diseño tambien era personal -un documento, un numero de jubilacion-, la sugerencia salia con
aplicabilidad = list(rut = ~ pila == "Ana")o~ edad >= 66aunquepilayedadestuvieran protegidas: la evidencia se tapaba antes de que la proteccion de la ausencia la leyera. Lo mismo con un nombre de columna con acento grave. -
Un valor citado entre comillas en la prosa del paquete se compara como un valor. El nivel de un determinante que no es personal -
proveedor == "juanperezsrl"- llevaba el nombre del titular protegido pegado a otras letras, y en la prosa se exigen limites de palabra. Ahora se tapa la cita, y la sugerencia se sigue leyendo. Los niveles citados escapan comillas y barras: un nivel con comillas daba una sugerencia que no era codigo de R valido. -
perfilar_por()avisa si una etiqueta de grupo lleva un valor protegido de otra columna, y lo declara enetiquetas_personalescon tipocontiene_valor_protegido. La etiqueta se sigue publicando, igual que cuando la columna de agrupacion es personal: es el eje del resultado. -
Las variantes de un nombre protegido se reconocen fuera del latin occidental: el vietnamita escrito sin sus marcas, el griego, el cirilico y el armenio en otra caja, y el ancho completo. La comparacion iba por bytes y el pliegue de caja era solo ASCII. Un mapa de diacriticos generado de la descomposicion de Unicode (
data-raw/mapa_diacriticos.R) y el pliegue de caja del paquete, que no depende del locale, la hacen por caracteres. - Un nombre protegido en latin1 sin marca se reconoce en la misma celda escrita en UTF-8. Lo que no declara codificacion y no es UTF-8 valido se lee como latin1 al comparar.
La proteccion de datos personales, mas rapida
-
perfilar()tarda la mitad sobre tablas grandes con datos personales. Sobre una tabla sintetica de 19 columnas, de 354 a 188 s en 800.000 filas. La proteccion formateaba los numeros y textos valor por valor, y cada valor protegido recorria todos los textos de la salida; ahora se formatea en una pasada por clase y un prefiltro por bytes descarta de entrada los valores que no pueden aparecer. El resultado es identico -medido sobre perfiles, planes y con coma decimal-. - La clasificacion y la proteccion de datos personales figuran como etapas en la medicion de tiempos de
perfilar_dbi(): corrian fuera de toda etapa, y sobre 800.000 filas eran tres cuartos del tiempo sin atribuir.
Ronda 18: la proteccion de datos personales, refutada
-
El nombre de una persona dentro de un texto libre de otra columna se enmascara aunque este pegado a otras letras. La tercera evaluacion habia llevado a exigir limites de palabra en toda la salida, para no tapar la prosa del paquete; una refutacion mostro que asi se publicaba
juanperezsrl@correo.uyfrente al titular protegido. Los limites se exigen ahora solo en la prosa del paquete -descripcion, sugerencia, motivo-, y en los valores vuelve la regla fuerte. -
Una variante sin tildes del nombre protegido se enmascara.
juan.perezse publicaba frente a «Juan Perez» con tilde: las agujas conservaban la letra acentuada. Ahora se comparan transliteradas, sin depender del locale. - Queda documentado por que el vocabulario -tipos de hallazgo, estrategias, nombres de diagnostico y los factores de un marco propio- no se enmascara aunque coincida con un valor protegido: es estructura, como los nombres de columna, y no dice nada de ninguna fila.
Ronda 17, normalizacion de texto
-
La eñe y la dieresis se protegen tambien en mayuscula.
AÑOse normalizaba aanoyañoaaño:CAÑADAno encontraba aCañadaen el referencial, yPEÑAse juntaba conPenaen vez de conPeña. -
Bajar a minusculas no depende del locale. Fuera del latin se usaba
tolower(): el cirilico, el griego y el latin de ancho completo se bajaban en un locale UTF-8 y no bajoC, ydetectar_claves()daba otro veredicto. Un mapa explicito cubre ahora esos alfabetos; lo que queda afuera se conserva. -
detectar_claves()cuenta como distintos dos textos con bytes invalidos distintos -antes eran uno, igual a un vacio-, y una clave compuesta no se confunde con un valor que trae el caracter separador. - La ligadura de s larga y t es «st»; las comillas de cierre dan la misma clave por los dos caminos de la normalizacion; y el referencial escapa la barra al pegar sus filas, para que su evidencia de proximidad no publique «distancia 0» junto a un veredicto que dice que la fila no esta.
Ronda 17: los diagnosticos numericos
-
Una numeracion por encima de 2^53 no se publica como no densa. La secuencia entera no se mide sobre numeros redondeados, pero publicaba tres veredictos
FALSEsin declarar nada. Ahora quedan enNA, con su fila en la cobertura, igual que cuando faltabit64. - El hallazgo de enteros fuera de la precision de un doble decia «la columna integer64» tambien sobre una columna de texto; y el motivo de Benford publicaba «3.000 < 3» sobre 2,9996.
Tercera evaluacion sobre bases reales
-
La proteccion de datos personales no borra el vocabulario del paquete. La proteccion enmascara por contenido, y un nombre de persona contenido en
faltantes_disfrazadosdejaba el tipo del hallazgo como[valor protegido]: en una base real, seis hallazgos sin tipo y, reproducido, el plan sin ninguna accion para ellos. Los tipos de hallazgo, las severidades, las estrategias y los nombres de diagnostico o de metrica son estructura, como los nombres de columna, y ya no se enmascaran. La proteccion de los valores no cambia. - La prosa del paquete no se tapa por azar. La forma sin separadores de un valor protegido tapaba la celda entera donde apareciera, y con los nombres de millones de personas alguno aparecia cruzando palabras: en esa misma base, 38 de 64 descripciones y 37 de 64 sugerencias tapadas, tambien de columnas que no eran personales. Ahora la variante tiene que estar sin letras pegadas; el documento escrito con separadores y el nombre dentro de un correo se siguen tapando.
-
perfilar_dbi()dice lo mismo queperfilar()sobre lo que protege. El motor escribia siempre “estadisticos de orden y la media protegidos”, aunque no hubiera media; y sus avisos de derrame decian “Metodo: .” en la moda y la mediana.
Ronda 15: claves, relaciones y el referencial
-
El referencial empareja numeros por su valor. Pasaba los valores a texto con 15 cifras:
0.1 + 0.2y0.3coincidian, yCorrectitudSemFuertedeclaraba conforme un identificador que el padron no tenia; y el mismo numero guardado como entero en una tabla y como doble en la otra -100000Ly1e5- no se encontraba.EntidadDuplicadatenia el mismo defecto: dos claves dobles distintas eran la misma entidad. -
detectar_claves()no prueba columnas que su propio filtro excluyo. Con una sola columna analizable que no era la primera,combn()probaba todas las anteriores, y un importe con decimales o una columna lista salian como clave. -
Una columna cuyo nombre se repite no se mide como si fuera otra.
sugerir_clave()publicaba la segunda con las cifras de la primera,detectar_relaciones()le atribuia una relacion que no existe ydetectar_dependencias()publicabak -> k. Ahora no se analizan y se declaran: una fila sin cifras ensugerir_clave()(yelegir_clave()no la ofrece), un aviso endetectar_claves(), el parsin_compararconmotivo_poda = "nombre_repetido"endetectar_relaciones(), y el motivonombre_repetidoen las dependencias, queperfilar()declara en su cobertura. -
detectar_relaciones()no compara una fecha con una fecha-hora. Las dos escrituras no coinciden nunca, y la fila deciasin_coincidenciascon cobertura 0 sobre los mismos dias. Salesin_comparar, conmotivo_poda = "fecha_contra_instante".
Ronda 16: el historico entre sesiones y las geometrias
-
La configuracion de una corrida no depende de como imprime la sesion. En R 4.6,
as.character()de un doble depende descipen(1e+07o10000000) y deOutDec(0,5). La configuracion se guardaba asi: la misma corrida medida con otroscipenno se podia acumular, y la deriva publicaba como error un cambio de configuracion que no ocurrio. Ahora se escribe con las opciones por omision, lo que conserva lo ya guardado. Las etiquetas de numeros deperfilar_por()y el emparejamiento del referencial tampoco dependen de esas opciones. -
guardar_historico(),guardar_analisis()yreportar()rechazan un destino que es un directorio. El archivo quedaba adentro con nombre de temporal y la funcion devolvia la ruta del directorio. - La deriva no dice que cambio un tipo cuando cambio el conjunto de metricas, y no afirma en una fila que se mantienen las comparaciones que otra fila retiro. El diagnostico de una serie sin par nombra su tabla.
-
aplicabilidadsobre una columna de geometria.perfilar()yanalizar()abortaban si la regla dejaba afuera la primera fila de unasfc, y en las demas posiciones las filas excluidas se volvian geometrias vacias que contaban como un valor mas (tasa_distintos = 1.5). Ahora se recortan al universo. -
Una geometria escrita como texto no recibe diagnosticos de palabras. Una columna WKT o WKB reconocida recibia ademas la proximidad de vocabulario,
posible_identificadorypatron_raro; ahora no, y la cobertura lo declara. Y se reconoce por mayoria de la muestra: un solo valor corrupto dejaba de reconocerla, segun donde cayera.
Ronda 15, segunda parte: estimaciones, senales, patrones y correos
-
Una columna con correos se protege aunque no sean la mayoria. La forma de correo protegia solo si el 80 % de los valores lo eran: una columna de nombre neutro con 70 correos y 30 textos publicaba las direcciones enteras en los ejemplos de sus patrones, en
perfilar()y endescubrir_patrones(). -
medicion_desde_estimaciones()produce una medicion queevaluar()acepta. Declarabareal-una proporcion en [0, 1]- para la estimacion, el error estandar, el coeficiente de variacion y los demas, yevaluar()rechazaba entera cualquier medicion con una estimacion mayor que 1. Ahora sonnumero_real. Ademas lee un factor por el texto de sus niveles -antes publicaba sus codigos como estimaciones- y descarta, con aviso y enattr(, "celdas_descartadas"), las celdas que no son numeros o estan fuera del dominio de su estadistico (un tamano de muestra de 0,5; un error estandar negativo). -
La evidencia de
detectar_discordancias()no se contradice. Conmax_ejemplos = 0decia “sin filas discordantes” al lado den_discordantes = 2; y dos dobles distintos que se escriben igual con 15 cifras se citan con las cifras que los distinguen. -
clasificar_variables()no llama entero aInf: una columna sin valores finitos quedadesconocida. Yn_niveles_observadosesNA, no 0, en las escalas que no guardan niveles. -
Un patron no publica letras ni digitos del valor.
descubrir_patrones()dejaba literales los digitos y las letras no ASCII -“José” dabaAa+é-. Ahora valen las clases de Unicode, con el mismo resultado bajo cualquier locale.
Ronda 14: fechas, validadores y perfil por grupos
-
Una fecha que la deteccion excluye no entra en el resumen. En
%Y%m%d, un99991231quedaba fuera del formato -el rango admitido es 1800 a 2100- pero el perfil lo publicaba comomaximo_fecha. Ahora la deteccion y el resumen usan la misma regla de validez, que tambien rechaza el segundo 60 y no deja salir el aviso crudo destrptime()ante un huso imposible. -
Una fecha que los dos formatos confirmados leen distinto no se convierte. En una columna mixta d/m y m/d,
01/12/2020quedaba en el formato de la mayoria, y agregar una fila que no estaba en ningun extremo movia el rango cinco meses. Ahora queda sin convertir, y el plan bloquea la conversion mientras haya ambiguos. -
Los validadores responden por el dato y no por su escritura en R. Un doble se valida por sus digitos (
12000000era una cedula invalida, y conscipenlo era cualquiera); las mayusculas son solo ASCII (toupper()volviaſeun codigo ISO); una URL marcadalatin1vale lo mismo que en UTF-8, yNaNes ausente. -
perfilar_por()etiqueta cada grupo con su valor. Uninteger64por encima de 2^53 salia con los bits crudos (4.45e-308), un complejo fundia dos valores, y una fecha o fecha-hora se etiquetaba con su numero de dias o de segundos. -
perfilar_por()declara todo lo que no perfila. Un grupo perfilado sin hallazgos aparece encobertura_grupos, para que la reconciliacion de filas cierre; un nivel(ausente)declarado y vacio se declara aunque hayaNA; un factor conNAcomo nivel ya no aborta. Los nombres de columna repetidos y unmin_filasfraccionario se rechazan con un error claro. -
El veredicto de
comparar_equivalencia()sale de la diferencia que publica. En el borde, las dos cuentas diferian en el ultimo bit, y la misma fila deciaequivalentecon una diferencia mayor que la tolerancia.
Ronda 13: reparacion de texto, validadores y deriva entre perfiles
-
validar_url()rechaza tres formas que no son sintaxis:[:::]como host IPv6, un host con una etiqueta vacia al final (ejemplo.uy..) y una@sin codificar dentro del usuario. -
Un documento uruguayo con un separador al borde no es un documento. Los separadores se quitan solo entre digitos:
"-12345672"era una cedula valida aunque la documentacion promete no quitar el signo para hacerlo pasar. -
Un texto mal convertido que el motor no puede reparar produce su hallazgo. Si no traia el caracter de reemplazo quedaba fuera de
n_codificacion_rota, y el perfil publicaban_codificacion_no_se_pudo = 1sin hallazgo ni accion. -
La deriva declara un cambio de
cadenas_ausenciay un cambio de tamano. Con los mismos datos, declarar la lista en una sola corrida publicaba una mejora o un deterioro que no ocurrieron; y una tabla que duplicaba sus filas no dejaba rastro en la deriva. - Documentado: las cifras de codificacion no son una particion; la decodificacion de entidades HTML quita una capa; los validadores recortan los espacios al borde, salvo
validar_url().
Ronda 12: lo que afirma el perfil, y el informe que lo comparte
-
Una columna vacia escrita con dos formas de ausencia cuenta lo mismo en todos lados. El hallazgo
constantedecia «los 4 que hay» en la descripcion, contaba 3 -la frecuencia de la forma mas comun- y trazaba 4, y el paquete se acusaba a si mismo sobre datos sin tocar. Ahora cuenta todas las formas, y la evidencia lo dice. - El informe dice que una dependencia se midio sobre la muestra. El objeto y la consola lo declaraban; el HTML publicaba «exacta» sobre una tabla grande donde la relacion se cumplia en la mitad de las filas, porque la muestra solo veia las que la cumplian.
-
?perfilardeclara dos decisiones que no estaban escritas: el tipo de una columna no textual es el de almacenamiento aunque no tenga valores, y la ausencia estructural se publica solo si la regla se cumple en el 99 % de las filas.
Ronda 11: el motor contra la memoria, y los agregados encadenados
- El alcance de un agregado de segundo nivel es el de cada entidad. Se emparejaba por el nombre de la metrica, que desde el segundo nivel comparten todas las entidades, y cada una recibia la suma de todas: «6 de 8» donde cada una midio 3 de 4, y se duplicaba en cada nivel.
-
Una corrida es un
id_mediciony unafecha. Dos corridas con el mismo id y fechas distintas se mezclaban enagregar()y en el tablero sin aviso. -
Un
NaNdel motor no se publica como una cifra calculada. duckdb conservaNaNcomo valor y respondiaMAXyAVGconNaN: salian enNAcon estadocalculado. Ahora las metricas de magnitud de esa columna quedanno_disponiblecon el motivo verdadero -antes culpaba a la magnitud de la columna-, y la mediana tambien: el motor ordenaba elNaNcomo el mayor valor. -
La sonda de magnitud usa
MAX, que ve unNaNen duckdb y PostgreSQL y un texto colado en una columna numerica de SQLite; conMINno veia ninguno de los dos. -
?perfilar_dbideclara lo que el motor guarda distinto que R: las fechas en SQLite -con el remedio,extended_types = TRUE-, elNaNen duckdb y la media en coma flotante sobre magnitudes muy distintas.
Ronda 10: lo que publica un metodo propio
-
Una
filaque no existe deja la metricano_medible. Sin recorte poraplicabilidadno habia tope:fila = 99sobre cuatro filas se publicaba como medida y viajaba al historico, y una posicion mayor que 2^31 saliaNAdespues de la validacion. -
La etiqueta
objetode un metodo propio no publica un valor personal. Con la proteccion activa y columnas personales en las tablas que recibemedir(), se reemplaza por la etiqueta canonica del paquete (tabla$columna[fila]): barrerla contra los valores protegidos no escala a millones de filas. Un valor que el metodo trae de otro lado -una tabla que no recibiomedir()- no se puede reconocer, y queda documentado como responsabilidad de quien escribe el metodo.
Ronda 9: el contrato de medir() y lo que aplica el plan de limpieza
-
Una salida que no cumple el contrato de
metododeja la metricano_medible, igual que un metodo que aborta, en lugar de abortarmedir()entero y llevarse la medicion de las demas. -
El atributo compuesto se reconoce aunque una columna traiga
+en el nombre: se busca si el valor se puede cortar en columnas recibidas. Antes,a+b+c+vcon una columnaa+bse rechazaba siendo legitimo. -
filatiene que ser una posicion entera. Un factor publicaba sus codigos y un texto saliaNA, la clausula que la validacion decia hacer cumplir. -
Con
aplicabilidad, lafilade un metodo propio se traduce a la tabla original. El metodo recibe la tabla recortada y escribia posiciones del recorte: la medicion decia fila 6 cuando habia medido la 9. -
El motivo de una metrica
no_mediblepasa por la proteccion de datos personales: trae el mensaje del metodo, que puede incluir un valor de la tabla. -
modelo()reconoce el mismo instrumento con los centinelas en otro orden (valores_nulos,diccionario,valores, que se usan como conjunto). En las propiedades donde el orden importa, comocoeficientes, no. - La clave de duplicados no se confunde con un separador dentro de un nombre, y un universo vacio ya no se publica como una tabla de cero filas.
-
guiar_limpieza()conserva la regla de aplicabilidad al agregar el diccionario: la reemplazaba y el plan guiado convertia la columna entera. -
n_no_reversiblesse recuenta sobre el resultado restaurado: contaba perdidas que la restauracion del universo ya habia deshecho. -
eliminar_filas_ausentesfunciona despues de otra eliminacion cuando no hay regla de aplicabilidad; fallaba culpando a una regla que el plan no declaraba.
analizar() sobre millones de filas: el detalle de la medicion ya no se arma entero
-
analizar()mide metrica por metrica. Sinconservar_detalle_medicion, media todo el modelo junto y materializaba una fila por celda y por metrica antes de agregar: en una tabla de 19 columnas, 21 filas de medida por fila de datos. Sobre 4,24 millones de filas eso mato el proceso con 103 GB en una evaluacion real. Ahora cada metrica se mide, se agrega y se descarta antes de la siguiente. Medido en 100.000 filas: de 2,41 GB a 1,00 GB de pico, y de 325 a 241 segundos. El resultado es identico: se comparo contra el camino de siempre, que sigue vivo conconservar_detalle_medicion = TRUE. -
Si ninguna metrica produce medidas,
analizar()ya no aborta. Tiraba el perfil, el plan y todo lo demas; ahora el tablero sale vacio con el motivo de cada metrica encobertura_metricas, y si se pidio evaluar, avisa que no evaluo.
La proteccion de datos personales tapaba numeros que son estructura
-
Un conteo, un indice de fila o un tamano ya no se tapan por coincidir con un valor protegido. El piso numerico tapaba cualquier numero cuya representacion fuera un valor protegido. En una base real de millones de filas, los
indices_filade una traza que coincidian con un documento quedaban enNAy lupa se acusaba a si misma -«total de traza no coincide con sus indices»-; y enperfilar_dbi()una columnaid = 1..nhacia que el conteonsalieraNAen todas las columnas. Es la misma propiedad que ya habia roto los nombres de columna. -
La lista es de estructura, no de valores, asi que falla cerrada: un campo nuevo que no este en ella se sigue tapando. Salio de recorrer los 271 campos numericos que publican
perfilar(),perfilar_dbi()yanalizar(). Lo que puede llevar un valor de la tabla -minimo, maximo, media, mediana, valor, coordenadas- se sigue tapando. En la prosa de un motivo, un numero que coincide con un valor protegido tambien se sigue tapando: ahi no se puede distinguir un conteo de un dato, y el numero verdadero viaja en su campo.
Ronda 8: el modelo de calidad y el informe dicen lo que sostienen
-
El contrato de
metodose hace cumplir, no solo se declara.medir()rechaza la salida con mas de una observacion para el mismo objeto, con una entidad no ligada o sinfilaen una metrica por celda o por fila. Medido: un metodo que devolvia dos filas por celda hacia queagregar(, "ratio")diera 0,5 donde la respuesta es 0,667. Y rechaza un atributo que no es una columna de las tablas que el metodo recibio. No se compara contra los atributos LIGADOS: las metricas de vigencia publican con razon la columna del contrato, yCorrectitudSemDebilpublicadni+nombre. Un recorrido de la suite -650 llamadas, 44 metricas- mostro que ningun metodo del paquete viola ninguna de las cuatro. -
La cobertura de una coleccion dice por que falta cada tabla. Con una frontera de
coleccion(), una tabla vacia y una que no estaba en la entrada recibian el mismo motivo generico, aunque la medicion sabia que la vacia tenia cero filas: lo dejo encobertura_metricas, y ahora ese motivo viaja a la cobertura. -
El README dice lo que cuesta la escala. Sobre tres tablas de entre 3,7 y 4,3 millones de filas, medidas en una evaluacion externa,
perfilar()tardo entre 14 y 59 minutos -y la misma llamada, repetida, 53 y 29- yanalizar()20 y 97. Solo lo que sostienen los datos entregados: un pico de memoria que se informo aparte no quedo registrado en ellos y no se publica. -
alcance_medidascuenta el universo de la metrica. Con la propiedadaplicabledeNoNulo, una metrica que midio sus tres filas aplicables publicaba «midio 3 de 4 en el universo aplicable». Y la causa «las que no tienen valor no producen medida» se afirma solo si las cuentas la sostienen. -
modelo()rechaza el mismo instrumento declarado dos veces con otro nombre: la medicion publicaba cada celda dos veces. Se compara conidentical()y no por texto, para no rechazar dos reglas con el mismo cuerpo y entornos distintos. -
La evolucion del historico en el informe sale de la deriva. Un
diff()propio publicaba el delta de un par que la deriva declara no comparable -«no se publica la comparacion del resultado»- dos secciones mas abajo. - Una medicion sin
id_medicionya no publica «0 corrida(s)». - Dos motivos que contradecian a su estado:
cobertura_analisis()sobre un marco propio que el perfil no mide, y «la entidad dependiente» en metricas que no tienen dependiente. Y pasar la propiedadmetododeEntidadContradictoriaal instanciar dice ahora donde se fija.
Un metodo que falla ya no se lleva la medicion de las demas metricas
-
medir()no aborta cuando el metodo de una metrica falla.?medirprometia que una metrica que no puede medirse deja su motivo encobertura_metricas, y cinco metodos del catalogo llamabanstop():ErrorEstandarsobre una columna sin dos valores,Escalacon valores no finitos, las dosOportunidadAtributoPor*sobre una columna que no es fecha, y cualquier metrica con un atributo que la tabla no trae. El error mataba la corrida entera, y con ella la medicion de las otras metricas del modelo. Ahora la metrica queda encobertura_metricascon el estado nuevono_medible, el motivo conserva el mensaje del metodo, y las demas se miden igual. - Se envuelve la llamada, no los cinco metodos. La propiedad es «un metodo que falla no tumba a los demas», y asi alcanza tambien a los metodos que escribe el usuario, que tenian la misma puerta.
-
Es un estado nuevo y no
contrato_incompleto, porque no dice lo mismo: el contrato esta completo y son los datos los que no admiten la metrica. -
Y
medir()avisa. El error era la unica senal; sin el aviso, un atributo mal escrito habria dejado una medicion con menos filas y en silencio. Lo que se valida antes del metodo -los argumentos demedir()- y despues -la forma de su salida- sigue abortando.
La proteccion de datos personales enmascaraba nombres de columna
-
Un nombre de columna ya no se enmascara, aunque un valor protegido caiga dentro de el. Lo encontro una evaluacion sobre una base real de millones de filas: la proteccion reemplazaba el valor protegido en TODA la salida, incluidos los campos que guardan nombres de columna, asi que
fecha_nacimientosalio publicada comofecha_[valor protegido]. Las dos guardas que comparan los nombres del perfil con los de los datos abortabanplanificar_limpieza()yanalizar(), y bastaba UN valor entre millones. El laboratorio no lo habia reproducido: ninguna prueba ponia un valor personal que coincidiera con un pedazo de un nombre. -
La proteccion va por campo, y el campo se reconoce por su contenido. No podia ser «proteger los nombres en todas partes»: en el caso minimo que lo reproduce, el VALOR
"documento"es igual al NOMBRE de columnadocumento, y proteger el nombre en todos lados dejaba de enmascarar la moda de esa columna y filtraba el valor. Un campo es de nombres si todos sus valores son nombres de la entrada, solos o unidos por los separadores del paquete; una lista de campos escrita a mano se habria quedado corta con el primero que se agregara.
La cifra que se rompio tres veces por ser una resta, y una identidad que se verifica
-
«Se calculo sobre N valores» ahora se CUENTA donde se calcula el resumen. Se derivaba como
aplicables - faltantes - excluidosy se rompio tres veces por eso: elNaNesta en dos de los tres sumandos y se restaba dos veces; el arreglo de eso -descontarn_nanuna vez- supuso que todoNaNesta enn_faltantes, y eso es falso para un"NaN"de TEXTO, porqueis.na("NaN")es FALSE y la columna lo cuenta como presente. Medido:c("10","20","NaN","30","40")publicaba «sobre 5» conmedia = 25, que es el promedio de cuatro. Una cifra derivada de otras tres hereda todas sus definiciones de presencia; contarla en el unico lugar que sabe cuantos valores uso la vuelve una medicion. -
El desglose dejo de afirmar como cuenta la columna lo que quedo afuera. Decia «
NaN, que la columna cuenta como ausente» y eso es falso para elNaNde texto, donde la misma fila publican_faltantes = 0. Quien cuenta la presencia esn_faltantes, que esta en la misma fila. Los infinitos si declaran presencia, porque unInfsiempre esta presente. -
unidadse calla tambien en las columnas compuestas. La rama de tipos compuestos escribe su propio estado -tipo_compuesto_no_analizado- y publicaba la unidad sin condicion, esquivando un discriminador que miraba solono_aplica: una matriz conattr(m, "units") <- "kg"publicabaunidad = "kg"junto amedia = NA, la forma exacta que el arreglo anterior habia venido a cerrar. Los dos estados que significan «esta clase no produce resumen» estan ahora nombrados en un solo lugar. -
La identidad de la celda del tablero se usa para AGRUPAR, y se verifica. Las granularidades sin rama propia agrupaban solo por
objeto_medible: dos entidades con el mismo objeto se fundian en una celda con la entidad de la primera y el promedio de las dos -medido,0.5donde habia un 0 y un 1-. Y la ruta de mediciones ya agregadas convertia cada fila en celda sin verificar nada, asi que dos filas con el mismo trio salian como dos celdas de identidad identica con valores 0,9 y 0,3. Ahora la entidad entra en la clave de agrupacion y la unicidad se comprueba, nombrando las celdas que chocan. -
Los pesos por posicion publican una etiqueta que vuelve a su parte. La via posicional esta documentada, pero la etiqueta solo se armaba en la rama de pesos nombrados y caia al defecto
medidas$entidad: sobre un origeninstancia*, seis pesos con dos nombres. Rehacer el numero por el nombre publicado daba 0,9 contra el 0,8 publicado, ypartes_con_peso_ceronombraba una entidad que si habia aportado con sus otras filas. Ahora la etiqueta esobjeto_medible, unica por fila. -
Dos declaraciones distintas ya no colapsan al mismo identificador. La tabla literal
m1.d-declarada conesquema = NA, la forma documentada para nombres con puntos- y la tabladdel esquemam1producen la misma cadena, y todo lo que resuelve por identificador se quedaba con la primera sin avisar. Se rechaza al declarar, nombrando las dos que chocan. -
meta$establesale de la bitacora y no de si el argumento vino. Conorden = list(t11 = "id")sobre dos tablas, la lectura det09salia sinORDER BY-la bitacora lo declaraba- y el objeto publicabaestable = TRUE; con una lista sin nombres, ninguna tabla recibia orden yestableseguia enTRUE. Ahora hay cuatro formas denota_orden, y la parcial nombra las tablas que quedaron sin orden. -
Y
sin_valoresse lee contra el universo que el resumen mide. Un refutador denuncio que el estado contradicen - n_faltantescon aplicabilidad declarada y propuso cambiarlo; medirlo mostro que el estado es correcto y que lo incompleto era la relacion documentada: con aplicabilidad, la que se cumple esn_aplicables - n_faltantes, y los valores de afuera del universo se declaran enn_presentes_fuera_de_aplicabilidad. Se corrigio la promesa y la prueba, no el estado.
Lo que abrio publicar la unidad: el NaN con dos signos, una cifra restada dos veces y una duracion muda
-
El
NaNse contaba con signos opuestos en dos canales de la misma fila. Una columna de treintaNaNpublican_faltantes = 30yn_distintos = 0-el paquete cuenta elNaNcomo ausente- y al mismo tiempo una fila de cobertura que hablaba de «30 valores PRESENTES que no pudo usar». El conteo no cambia -los dos README prometen quen_valores_excluidos_resumencuenta los no finitos- y lo que se corrige es la afirmacion de presencia: el desglose separa ahora los infinitos «presentes y no utilizables» de losNaN«que la columna cuenta como ausentes», conservando el termino paraguas «valores no finitos». -
Y ahi habia una cifra publicada equivocada. El motivo decia «se calculo sobre 27 valores» donde los finitos son 28: la formula era
aplicables - faltantes - excluidosy elNaNesta en los dos ultimos, asi que se restaba dos veces. Con dosNaNpublicaba 26. Comprobado contra el conteo a mano en cinco configuraciones. -
La comparacion de perfiles fabricaba un cambio de unidad que nunca ocurrio. Trataba igual dos casos distintos: con unidad en los dos lados -
mcontrakm- el mismo numero significa otra cosa y eso sigue siendoerror; conNAen uno de los dos lo que cambio es si la columna DECLARA su unidad, y afirmar que «el mismo numero significa otra cosa» es falso cuando la entrega anterior no publicaba ninguna cifra. Ese caso salesospechosocon su propia redaccion, en las dos direcciones. -
unidadse publicaba sobre columnas que no publican ninguna cifra. El atributounitslo lleva cualquier clase: unfactorconattr(x, "units") <- "kg"publicabaunidad = "kg"junto amedia = NA. El campo promete decir en que unidad estan las cifras que la fila publica, asi que se calla cuando no publica ninguna. El discriminador esestado_resumen_cuantitativo == "no_aplica"y no una lista de clases: una columnaunitsde purosNAsi declara su unidad, porque su clase se resume. -
difftimequedaba mudo en tres diagnosticos. Con las MISMAS cifras, la columnadoblepublicaban_faltantes_disfrazados_numericos = 2y ladifftimepublicaba 0, las dos con la misma media distorsionada; ydifftimetampoco recibia fila de cobertura enley_benfordni enrelacion_aritmetica_columnas, mientrasunitsla recibe en las dos. Una sola causa:is.numeric()responde FALSE sobre undifftime, y el predicado general del paquete -escrito para no enumerar clases- descansa en el, asi que esa clase se caia por el hueco entre «numerico pelado» y «numerico con clase». Importa desde quedifftimepublicamediacomo cualquier numero: ahi su silencio dejo de ser legible. El criterio de la declaracion pasa a ser si la fila publica cifras cuantitativas;DateyPOSIXtsiguen afuera a proposito, porque su resumen sale enmedia_fechay no como numero pelado. -
Una fila podia identificar una tabla y traer los datos de otra. El adaptador de colecciones de otra generacion recalculaba
identificadoryreferenciasolo cuando FALTABAN, asi que una copia guardada que ya no corresponde a su nombre se usaba tal cual: con unareferenciaque apunta at2, el perfil publicabatabla = t1conn_filas = 7, que son las filas de t2. En silencio. Ahora lo derivado se deriva siempre -el nombre es la declaracion y esas dos columnas son consecuencia suya-, y medido antes de cambiarlo: sobre un objeto sano derivar da identico a lo guardado. De paso, la regla estaba escrita dos veces y ya habia divergido; queda en una sola funcion con una prueba que falla si alguien vuelve a duplicarla.
Cuatro decisiones tomadas: difftime publica, las hermanas se igualan y el estado se parte en dos
-
difftimepublica sus estadisticos en la unidad que declara. Era el unico que se abstenia -todo enNAyno_aplica- y su motivo escrito era que sin unidad publicada no se publica el numero. Con el campounidadese motivo dejo de valer, yDateyPOSIXtpublicabandesvioen segundos desde antes: seguir absteniendose era la inconsistencia. Una columna de minutos publicamedia = 75conunidad = "mins", en la unidad DECLARADA y sin convertir a segundos, porque convertirlos publicaria un numero que no esta en la columna. -
estado_resumen_cuantitativodistingue dos afirmaciones que compartian un nombre.sin_valoresera cierto de una columna vacia y de una de purosNA, y FALSO de una de treintaInf-que publican_faltantes = 0yn_distintos = 1- y de una donde los centinelas se llevaron todos los valores: ahi habia valores y ninguno servia. Medido: cinco situaciones distintas publicabansin_valoresy en tres el nombre mentia. Ahora esas tres publicansin_valores_utilizables, y la presencia se define igual que enn_faltantes-que cuenta elNaNcomo ausente-, para que dos campos de la misma fila no cuenten cosas distintas. -
normalizar_espacios_invisiblesse trata como su hermana. Las dos estan en la misma rama del planificador y se trataban al reves:eliminar_controles_invisiblesse recomendaba y se activaba sola, y esta era destructiva y exigia confirmacion. Convertir un espacio Unicode en un espacio comun deja un espacio -es cambio de forma, no de valor- y el paquete recomienda y activarecortar_espacios, que quita espacios enteros. La confirmacion tendria sentido si alguna pudiera tocar un invisible SIGNIFICATIVO, y ninguna puede: sus conjuntos de codigos son disjuntos del de ZWJ y ZWNJ, y eso lo sostiene una prueba.reversiblesigue enFALSEen las dos -quitar o convertir un caracter no se deshace- y lo que se pierde de verdad, dos valores distintos que quedan iguales, lo sigue contandon_no_reversibles. -
Que la moda no se compare en
comparar_perfiles()queda escrito como decision, no como pendiente: es un valor y no una propiedad medida, y cambia con cualquier corrimiento normal de los datos.
La unidad viaja al lado de la cifra, y la comparacion la ve
-
La tabla de columnas publica
unidad. Cuatro columnas medidas en unidades distintas publicaban filas IDENTICAS:set_units(c(1,2,3), "m")y la misma en"km","m^2"o"kg*m/s^2"daban todasmedia = 2ydesvio = 1, y unPeriodde dos dias publicaba172800sin decir que eran segundos. El campo contesta una sola pregunta -en que unidad estan las cifras que la fila publica- y queda enNAdonde no hay ninguna: no inventa.difftimesigue absteniendose del resumen y su unidad viaja igual, porque abstenerse no es motivo para callar lo que la columna declara. ParaDateyPOSIXtpublicasegundos, que es la unidad dedesvioy que hasta ahora solo estaba dicha en prosa. -
Y
comparar_perfiles()lo compara como un aspecto propio. El campo solo no cerraba nada: el defecto era que «una entrega donde cambio la unidad no muestra cambio», y medido antes de escribirlo, con la unidad puesta a mano en las dos entregas, la comparacion seguia devolviendo cero filas porque su lista de aspectos es cerrada. Ahora un cambio demakmcon los mismos valores sale con severidaderror, la misma que un cambio de tipo, y un perfil guardado antes de que el campo existiera declara la parte como no comparable en vez de mentir. -
clasificar_variables()publicaba la unidad equivocada, y con unidades compuestas abortaba. La unidad de un objetounitsvive en unsymbolic_unitscuyo NUMERADOR es un vector: leerlo con[[1L]]publicabakmpara una columna enkm/h-no una unidad ausente, una equivocada, con la misma forma que la correcta- y conm^2-que guardac("m", "m")- devolvia dos valores, con lo que la fila de esa columna pasaba a tener dos filas y la funcion entera moria con «replacement has 2 rows, data has 3», sin devolver ni las columnas que si podia clasificar. Ningun fixture del paquete usaba un objetounitsde verdad -todos declarabanattr(x, "units") <- "kg", una cadena de largo 1, donde[[1L]]acierta-, asi que las dos puertas eran invisibles para la suite entera. La regla vive ahora en un solo lugar, compartido con la tabla de columnas.
La coleccion declara con que alcance calculo cada numero
-
La cobertura de una relacion se calculaba sobre menos filas de las que el objeto publicaba como evidencia.
relaciones_coleccion()no le reenviabamuestraadetectar_relaciones(), asi que conmuestra > 1e5valia el tope por omision de esa funcion: el objeto publicabafilas_leidas_1 = 200000ymeta$muestra_por_tabla = 2e5, y la cobertura salia de un submuestreo de 1e5 que no se declaraba en ningun campo documentado. La fila llegaba a contradecirse a si misma:n_valores_comunes = 99999, contado sobre la lectura entera, junto acobertura = 0, calculada sobre la mitad. Medido: sobre las 200.000 filas que la fila declara, la cobertura es 0,499995. -
Cada par declara su propio alcance, y aparece el total de la tabla. El total no estaba en NINGUNA ruta del objeto -una cobertura de 0,1 sobre 10.000 filas leidas no se distinguia de «10.000 de 10.000»-, y lo unico que sobrevivia eran los atributos del PRIMER par publicado, con etiquetas que no nombran a ninguno: con dos pares decian
muestreado = FALSEmientras el segundo habia leido 10.000 filas de 20.000. Ahora cada fila publicafilas_totales_*ymuestreado_*, y para saberlo se pide UNA fila mas que el tope -mucho mas barato que unCOUNT(*)por tabla, y exacto-: si vuelve, la lectura esta truncada y el total vaNA; si no, las filas leidas SON el total. La fila de sobra no viaja al resultado. -
Una coleccion guardada por una version anterior se perfila igual. El paquete ya habia arreglado esto para
catalogoy el recorrido encontro SEIS columnas en el mismo estado: sintipo,esquema,identificadorodeclaracionla corrida entera moria con un error dedata.frame()-«los argumentos implican un numero diferente de filas»- que no nombraba ni la columna ni la tabla, y sinreferenciaperfilaba CERO tablas sin abortar, que es peor. Ahora lo completable se completa con el valor que significa «no se declaro», lo derivado se recalcula con las mismas funciones que lo arman, y lo que no se puede inventar -tabla- se rechaza nombrando la columna. El arreglo es por recorrido y la suite quita cada columna de a una.
La celda del tablero dice de quien es, y el indice deja de afirmar lo que desmiente
-
La identidad de la celda era incompleta y dos celdas distintas publicaban la misma. El tablero publicaba
(metrica, objeto)ymetricaes el nombre GENERICO, asi que dos tablas con una columna del mismo nombre -o dos especializaciones de la misma generica sobre la misma columna- salian como dos filas con identica identidad y valores distintos: rehacer la celda con las claves publicadas tomaba las medidas de las dos y daba 0,667 contra celdas de 1,0 y 0,5. Ahora se publicanmetrica_instanciadayentidad. La instancia sola no alcanzaba: sobre un agregado valeagregada:ratio:<metrica>para todas las filas, y ahi la entidad es lo unico que separa. Y para quien lee, con varias entidadesobjetonombra la tabla -cod (tabla: t1)-, en el mismo idioma con el que la granularidad de tabla ya publicaba(tabla: t1): la convencion estaba escrita y la rama que colisionaba recibia el dato sin usarlo. -
advertencia_universosse calcula, ya no es una frase fija. Afirmaba «los componentes salen de universos distintos» en toda corrida, incluso con los tres componentes publicandouniverso = "celdas"-una afirmacion que el propio objeto desmentia en su columnauniverso- e incluso por la rama «no hubo componentes combinables», donde no hay ningun componente del que afirmar nada. Y no nombraba nunca ningun componente, que era justo lo que prometia declarar. Ahora: con un solo universo lo nombra y dice que las unidades son comparables; con varios dice cuantos son y nombra los componentes de cada uno; sin componentes no afirma nada. La impresion sigue al contenido: advertencia solo cuando hay heterogeneidad.
El agregado no puede tirar lo que la medicion declaro, y el promedio mira el tipo
-
La guarda del orden temporal se apagaba al agregar.
agregar()arrastraba los atributos de la medicion con una lista escrita a mano, y en esa lista faltabafecha_declarada..exigir_orden_temporal()empieza conif (!isTRUE(declaradas)) return(), asi que sin el atributo la guarda volvia en silencio: las mismas dos entregas con fecha declarada e invertida DETENIANcomparar_evaluaciones()por el camino demedir()y publicabandelta = -1por el camino deagregar()-el delta con el signo al reves, que es exactamente lo que esa guarda existe para impedir-. Ahora las dos listas -lo que viaja y lo que a proposito no viaja, con su motivo- se declaran juntas y la suite RECORRE los atributos quemedir()pone de verdad: un atributo nuevo que no este decidido en ninguna de las dos hace fallar la prueba. La lista ya habia sido la guarda tres veces. -
alcance_medidasviaja al agregado y al tablero, reexpresado en su clave. El «midio tres celdas de cuatro» moria en el primer salto, asi que el tablero publicaba0,667sin nada que lo distinguiera del0,667sobre cuatro. Arrastrarlo tal cual no alcanzaba: la tabla esta indexada pormetrica_instanciada-Formato@t.cod- y el agregado renombra la metrica aagregada:ratio:Formato, con lo que la declaracion no se podia atribuir a ninguna fila. Los conteos se suman por objeto de destino y solo cuando todas las partes declaran la misma unidad; si no, la fila declara la mezcla en vez de publicar un total que no estaria en ninguna unidad. -
promedioypromedio_ponderadorechazan los tres tipos no acotados.metrica()promete quenumero_real,enteroyduracionno admiten las cuatro agregaciones normalizadas.ratioyratio_umbrallo cumplian con su guarda de tipo; los dos promedios no tenian ninguna y la unica guarda era por VALOR -«deben estar en [0, 1]»-. Una duracion de 0,25 y 0,75 dias se promediaba y se publicaba comotipo_resultado = "real", o sea como una proporcion, y la misma metrica con 1,5 dias abortaba: el mismo modelo cambiaba de conducta segun los datos que le tocaran. El mensaje nombra la metrica que lo viola. -
Los pesos se publican y el peso cero se declara en todos los destinos. El objeto trae
pesos_declaradoscon el nombre de la parte que recibio cada peso, asi que el numero se rehace con lo que el objeto publica. Ypartes_con_peso_cerose decide por la propiedad -«alguna parte pesa cero»- y no por el destino: antes colgaba de un atributo de cobertura de frontera, y enconjuntoEntidades, que no tiene ninguno, el mismo peso cero que a nivel coleccion se declaraba pasaba en silencio, contado en la identidad de la fila (entidad = "t1, t2") sin aportar al numero.
La magnitud que perdio exactitud no se publica, y una columna compuesta cuenta filas
-
El entero de 64 bits que llega como doble deja las SIETE metricas de magnitud en
no_disponible. El manual lo prometia y la guarda recorria cinco:medianaydesviose calculan en otras consultas y saliancalculadoen la misma corrida dondeminimo,maximo,media,n_cerosyn_negativossalianno_disponible. ConUBIGINTcerca del tope eso publicabamediana = 2^64-un numero que no esta en la columna- ydesvio = 0sobre una columna cuyo desvio real es 1,29. La condicion es de la columna, no de la metrica. Lo que el motor cuenta con exactitud -n_distintos, la moda- se sigue publicando. -
Una columna cuyos valores son
data.frameo matriz cuenta filas, no campos.length()de undata.framees su numero de columnas, asi que una columnaSTRUCT(a, b)sobre una tabla de tres filas publicaban = 2mientrasgeneral$filasdecia 3. Por DBI, el bloque de muestra discrepaba del resumen SQL sin que nada declarara la diferencia. Tambien alineancon la mascara del universo aplicable, que llega con el largo de la tabla.
El universo aplicable manda en el conteo Y en la traza
-
La geometria no cuenta fuera del universo declarado. Con seis geometrias y dos declaradas fuera del universo,
coordenada_fuera_dominiopublicaba tres afectadas y la traza -que si recortaba- nombraba una:perfilar()emitia su propio avisolupa_trazabilidad_incoherente-«es un problema delupa, no de sus datos»- sobre un perfil intacto. El analisis geometrico corre sobre la columna entera porque necesita su CRS y sus tipos, asi que el universo se aplica despues, por indices, y cada contador se recalcula de su recorte.tipos_geometria_mixtos, que habla de la columna, cuenta ahora las geometrias del universo. - Y la traza de mayusculas no nombra de mas. El otro lado del mismo defecto: los grupos de caja se formaban sobre la columna cruda y se recortaban despues, asi que la traza nombraba una fila cuyo valor el conteo no contaba ni la evidencia citaba -colisionaba con un valor de una fila NO aplicable-. Ahora se forman sobre el universo, y la guarda de coherencia recomputa sobre el mismo universo: si las dos mitades no miran lo mismo, el aviso dispara aunque las dos esten bien por separado.
-
Tres campos de geometria quedan declarados como lo que son.
n_bbox_evaluados,n_geometrias_analizadasyn_vertices_analizadoscuentan el trabajo hecho, no filas de un universo, y no hay conjunto de indices del que derivarlos: se publican tal como se midieron y?perfilarlo dice. Recortarlos a ojo seria inventar un numero.
Conteos que describen lo que miraron, y una deriva que dice cuando no pudo comparar
-
El conteo de
fecha_partida_columnasya no sale de partir su propia evidencia. Con una tabla de TRES columnas llamadasanio+bis,mesydia, el hallazgo publicaban_afectados = 4yn_evaluados = 3en unidadcolumna:n_afectados > n_evaluados, que es imposible con un conteo honesto. El 4 salia de partir el texto de la evidencia por+. La variante con parentesis subcontaba, por el recorte que fundiaanio (provisorio)con la columnaanioreal. Ahora las columnas involucradas viajan por posicion desde el detector. -
Sin patron dominante suficiente, la ausencia se declara. El README y una vinieta prometen que si ningun patron dominante alcanza el umbral, eso queda en
cobertura_diagnosticos. Con veinte valores en cinco patrones y el mayor en 0,25 contra un umbral de 0,5, la salida no tenia ni hallazgo ni cobertura: la condicion exigia mas de una fila en el resumen de patrones, y el resumen que llega trae una. -
posible_centinela_numericodeclara el universo donde de verdad mira. Sobre la misma columna y en la misma corrida,ceros_no_permitidosyoutlierspublicabann_evaluados = 26-28 filas menos una ausente y una no convertible- y el centinela publicaba 28. Su deteccion corre sobre el resumen cuantitativo, igual que la de sus hermanos. Los diagnosticos que miran la columna entera -patron_raro,faltantes,filas_duplicadas- siguen en 28, que es su universo real. -
Una deriva vacia dice si fue porque nada cambio o porque no habia con que comparar.
detectar_deriva_calidad()sobre una serie de una sola medicion devolvia cero filas y ningun atributo, y el informe imprimia la seccion vacia sin decir que no habia par. Ahora el objeto publicacobertura_diagnosticoscon el motivo y el informe lo muestra, con la misma frase que el resto del paquete: su ausencia no es conformidad.
La cadena que se compara es la que el objeto declara
-
Dos columnas de texto con el mismo nombre ya no se comparan en silencio. El camino explicito rechaza los nombres repetidos, pero el automatico tomaba
names(datos)sin deduplicar y la seleccion los resuelve conmatch(): comparaba la PRIMERA columna contra si misma, publicaba evidencia con valores que la segunda no tiene y unexactoa distancia 0 donde la distancia de los valores reales -0,162- no llega al umbral. Es la tabla que produceread.csv(check.names = FALSE)sobre un archivo con encabezados repetidos. No se compara un subconjunto: se declara enrazony no se compara nada, igual que cuando una columna queda afuera por su largo. -
Un valor declarado
bytesya no se iguala al literal de su escape. El rendido de publicacion lo convierte ena\xc3\xb1o, que es una cadena que se puede teclear: el valor no textual y ese literal de diez caracteres quedaban identicos y el par saliaexacto_normalizadoconigualo_normalizar = TRUEincluso connormalizar = FALSE, donde no hay ningun mecanismo declarado para igualar textos. Ahora se rinde con la clave de bytes, que escapa la barra invertida antes que nada y por eso distingue los dos. El mismo texto en UTF-8 y enlatin1sigue siendo el mismo valor. -
El separador de concatenacion no puede salir de un valor.
c1 = "x | y", c2 = "z"yc1 = "x", c2 = "y | z"producian la misma cadena"x | y | z": dos filas que no comparten ningun valor salianexacto_normalizadoa distancia 0, mientras el informe de fusiones del propio objeto declaraba que ningun paso de normalizacion habia fundido nada. Ahora cada|dentro de un valor se escribe\|, y la clave de bytes se aplica una sola vez -la segunda aplicacion volvia a escapar lo que la primera habia puesto-, asi que la distancia de una comparacion de varias columnas se rehace con la receta que el manual publica. -
El limite de la descomposicion canonica queda escrito. El manual prometia que “se aplica siempre” y daba la receta “hay que descomponer primero”; la tabla cubre el subconjunto latino, asi que fuera de el dos escrituras canonicamente equivalentes no colapsan -el mismo nombre griego en NFC y NFD sale
aproximadoa 0,124- y la receta da otro numero. Ahora dice hasta donde llega y como se rehace cada caso. Lo sostiene una prueba, no el texto: si la tabla se amplia, se pone en rojo.
El libro de cobertura por grupos cierra, y la etiqueta de un grupo vuelve a su valor
-
cobertura_gruposreconcilia con las filas de la tabla. El grupo"(ausente)"aparecia en los hallazgos y en la cobertura, las dos veces con sus 40 filas, asi que la suma daba 120 sobre 80 filas reales y quien usaba la tabla para verificar que no se pierde ninguna fila -el uso que la funcion promete- no podia. La columna logica nuevagrupo_perfiladodice de que clase de grupo habla cada fila, y el@returnescribe como se hace la cuenta: sobre grupos distintos, porque hay una fila por grupo y motivo. -
La marca de esa fila se mide. Si el grupo fusionado queda por debajo de
min_filasno se perfila, y la fila lo dice; la primera version del arreglo la dejaba fija enTRUEy era falsa justo en ese caso. -
La colision de
"(ausente)"ya no se declara cuando no hay colision. Con el literal(ausente)como valor real y cero ausentes, la cobertura publicaba «junta 0 fila(s) con la columna de agrupacion ausente y 40 fila(s) cuyo valor real es el texto(ausente). Son dos cosas distintas bajo una sola etiqueta»: una afirmacion falsa dentro del libro que existe para poder confiar en el. La condicion mira ahora las dos cosas que una colision necesita. -
Un nivel de factor declarado sin ninguna fila se declara. Con niveles
A, B, C, Dy filas solo enAyB, la salida decian_grupos = 2y las dos tablas de cobertura quedaban vacias: nada explicaba la diferencia entre los cuatro niveles que la columna declara y los dos grupos que publica. Un nivel sin filas es un grupo de cero filas, o sea por debajo de cualquiermin_filas, ymin_filaspromete que eso se declara. -
La etiqueta de un grupo vuelve a su valor.
as.character()sobre un doble usa 15 cifras significativas:1e17y1e17 + 32daban los dos"1e+17"y caian en un solo grupo de 80 filas cuyos numeros no correspondian a ninguno de los dos -20 filas duplicadas contra 14 y 6 de cada valor por separado-. Ahora la etiqueta lleva las cifras que hacen falta para distinguirlos yas.numeric()de la etiqueta recupera el valor exacto. Para la enorme mayoria de los numeros la etiqueta es la de siempre:0.1sigue siendo"0.1".
El informe no publica el valor que viajaba unido, y la remediacion declara lo que cambia
-
El piso de datos personales se quedaba sin agujas y el informe publicaba tres de cuatro cedulas con su proteccion puesta. La cosecha tomaba la celda
ejemploscompleta, y esa celda trae hasta tres valores unidos con" | ": la aguja era la cadena unida, que no existe en ninguna otra parte de la salida, asi que el reemplazo no encontraba nada. Ahora la cosecha separa los valores. Con documentos numericos el mismo camino ya funcionaba -las agujas llegan porminimoymaximo-, y por eso las pruebas que reportaban un perfil abierto pasaban sin ver la fuga. -
El enmascarado reconoce dos formas mas del mismo valor. La que solo difiere en la caja de sus letras ASCII -el mismo nombre en minusculas dentro de un texto libre se publicaba mientras la forma canonica quedaba tapada- y la que comparte con un valor protegido una corrida de seis digitos o mas, como una cedula sin su verificador. El plegado de caja no usa
toupper(), que aborta sobre bytes que no son UTF-8 validos. La comparacion por corridas se limita a digitos: aplicada al texto taparia una palabra corriente por compartir un tramo con un apellido. -
Y esa comparacion por corridas no cuesta lo que costaba. La primera version preguntaba celda por celda contra aguja por aguja: sobre 8.000 celdas y 400 agujas tardaba 11,8 s, y con cuarenta veces mas agujas costaba treinta y dos veces mas. Las agujas se concatenan con un separador que no puede aparecer en una corrida de digitos, asi que una sola busqueda por corrida alcanza y el costo deja de depender de cuantas agujas haya: 0,10 s sobre el mismo tamano, contra 0,05 s antes de que la regla existiera. Lo vigila una prueba que afirma sobre la RAZON entre dos mediciones del mismo proceso y no sobre un techo en segundos. El tiempo de
checking testsno se movio con la optimizacion, asi que el costo de la funcion no era lo que lo explicaba. - La guarda del millon de valores medía el arranque y no el algoritmo. Su calentamiento era una tabla de una fila, asi que la primera corrida grande pagaba sola el costo de arranque -8,42 s contra 5,92 y 5,80 las siguientes, con el techo en 8- y la prueba se ponia roja sin que nada hubiera empeorado. Ahora mide el regimen y el arranque por separado, cada uno con su techo.
-
Una seccion que no se puede armar se declara y ya no mata el informe. Un
historico_calidadal que le faltaba una columna abortaba el documento ENTERO -perfil y mediciones incluidos- con el mensaje de R base “argumento 1 no es un vector”. Ahora la seccion nombra los campos que faltan y el resto se escribe. Es el unico objeto reportable que pasaba la puerta roto:medicionaguanta que le falte cualquiera de sus diecisiete columnas y unplan_limpiezarecortado pierde su clase, de modo que ahi el mensaje ya era el deliberado. -
winsorizar_outlierssobre una columna entera declara que deja de ser entera. Los limites de Tukey son cuartiles: la columna salianumericcon la fila diciendodestructiva = FALSEy ningun campo del registro nombrando el tipo, mientras la misma accion sobreDatesalebloqueada. Ahora se marcadestructiva, la justificacion lo dice yparametrospublicatipo_originalytipo_resultante. Sobre una columna doble no cambia nada. -
guiar_limpieza()devuelve el plan sin cambios tambien cuando esta editado. En el camino no interactivo y sinselectorreescribiadecision_grupoderecomendadaadesactivada: afirmaba que quien llama habia desactivado la recomendacion cuando solo la habia desmarcado, y esa afirmacion viajaba con el plan. La sincronizacion pasa a correr despues de la puerta de salida. -
El registro cuenta las celdas que cambiaron de codificacion en
n_codificacion_normalizada. Una accion de texto sobre celdaslatin1devuelve UTF-8 solo en las que toca, asi que la columna puede quedar con marcas mixtas: el valor es el mismo y los bytes no. No se convierte la columna entera a proposito, porque eso tocaria celdas que el plan no declaro como cambiadas.
Modelo y deriva: que cuenta como un cambio de modelo
- La descripcion del modelo que viaja con cada medicion (
configuracion_modelo) ya no depende del orden en que se declaro. Declarar las mismas metricas al revés, o los mismos pares dimension-factor de un marco, producia dos descripciones distintas y una serie publicabaconfiguracion_modelo / no_comparablecon severidad error sobre resultados identicos. El orden de losatributosde una instancia sigue contando, y es una diferencia medida: el metodo recibe las columnas en ese orden, asi que reordenarlos cambia lo que se mide. - El
metodode medicion que declara quien llama eninstanciar()pasa a ser parte del modelo. Dos corridas con metodos distintos daban la mismaconfiguracion_modeloy la deriva atribuia la diferencia a los datos (aspecto = resultado). El metodo por omision de cada metrica no entra en la descripcion: es del paquete, y serializarlo haria que cada version nueva de lupa acusara un cambio de modelo en toda serie existente. -
comparar_equivalencia()declara el campo registrado que uno de los dos lados no mide. Antes desaparecia de la salida entera -sin fila, sincampos_no_comparables, sin diagnostico- y elresumencontaba los campos comparados como si fueran el universo completo. Ahora queda encampos_no_comparables, con motivocampo_solo_en_anteriorocampo_solo_en_actualendetalle_campos_no_comparablesy su fila encobertura_diagnosticos. - Nota para series ya guardadas durante el desarrollo: las tres cosas cambian la huella de
configuracion_modelo, de modo que un historico anterior puede mostrar un cambio de modelo en el limite entre las dos versiones.
Los estadisticos declaran su universo, y una columna exotica no rompe el analisis
distribucion_valores()yanalizar()ya no mueren sobre una columna cuya clase R declara numerica pero cuyo cuantil interpolado no se puede calcular -lubridate::Periodes una: su aritmetica rechaza el producto por una fraccion-. La guarda excluia por una LISTA de clases y esa columna se llevaba puesto el analisis entero, mientrasperfilar()sobre la misma tabla sobrevivia. Ahora la condicion se MIDE intentando el calculo: la columna conserva sus filas convalorenNAy estado"no_interpolable:<clase>", y una clase nueva con la misma limitacion queda cubierta sin tocar el codigo.Una fecha no finita se declara excluida, y en las dos clases. Con un
Infen una columna de fechas, el resumen se calculaba sobre los finitos y la fila publicaban_valores_excluidos_resumen = 0con estadocalculados. Ademas el conteo de fechas resumidas estaba escrito dos veces -sum(!is.na(x))paraDate,sum(is.finite(x))paraPOSIXt- y sobre el mismo dato una decia 3 y la otra 2. Ahora las dos dicen 2 y las dos declaran la exclusion.integer64sin ningun valor que sobreviva dicesin_valores. Con todos los valores declarados centinela publicabacalculados_sobre_valoresjunto aminimo,mediaydesvioenNA: el estado se escribia apenas habia excluidos, antes de saber si quedaba algo. La rama de dobles sobre el mismo dato ya deciasin_valores.Las longitudes de texto declaran su universo.
nchar()no puede medir un valor cuyos bytes no son UTF-8 valido, y esos se descartaban sin contarlos: con tres valores, uno invalido, la fila publicabalongitud_media = 4.5-el promedio de DOS- y ningun campo lo decia. El campo nuevon_longitudes_resumidasdeclara sobre cuantos se calcularon, igual quen_fechas_resumidasdeclara el universo del resumen de fecha; la causa ya viajaba aparte enn_codificacion_invalida.
La ventana que no puede gobernar se declara
-
senal_redundante(ventana = )promete una tolerancia, y endetectar_discordancias()esa tolerancia solo se aplica si todas las columnas comparadas son numericas. UnDateno esis.numeric(), asi que las columnas temporales caian en la rama de igualdad exacta y la ventana declarada no se usaba: medido, dos columnasDatea un dia de distancia conventana = 1publicabann_discordantes = 2, contra 0 del control numerico con las mismas distancias, y la fila publicabaventana = 1al lado de una comparacion que no la habia usado.Ahora se avisa, y el aviso dice que hacer: llevar las fechas con
transformaciona un numero en la unidad de la ventana. El numero publicado no cambia -inventar una tolerancia sobre fechas seria definir algo que el paquete no define- y la evidencia ya lo declaraba concomparacion textual exacta. La documentacion deventanalo dice ahora en el@param, donde se lee antes de usarlo. La prueba incluye el camino que el aviso recomienda: con
transformaciona numero, la ventana gobierna y no hay aviso. Un consejo que no funcionara seria peor que el silencio.
Lo que no se declaro no es parte del modelo
-
detectar_deriva_calidad()existe para que una serie no confunda un cambio de datos con un cambio de modelo, y hacia justo eso en el caso mas comun: seguir el mismo modelo en el tiempo.vigencia()traefecha_acceso = Sys.time()por omision, asi que dos corridas del MISMO codigo llevaban dos configuraciones distintas. Medido, dos corridas separadas 1,2 segundos publicaban:aspecto = configuracion_modelo cambio = no_comparable severidad = errorsin que hubiera cambiado nada. Ahora el contrato registra que campos recibio declarados y la comparacion no mira los otros: dos corridas del mismo modelo comparan identicas, y declarar
fecha_accesolo devuelve a la comparacion, asi que cambiarlo si sigue siendo un cambio de modelo. La prueba exige las dos mitades -que dos corridas iguales comparen iguales y que dos momentos declarados distintos NO comparen iguales- y ademas comprueba que los dos momentos crudos difieran: la serializacion no lleva fracciones de segundo, asi que dos llamadas dentro del mismo segundo habrian pasado la prueba tambien sin el arreglo.
La suite dice en que archivo va mientras trabaja
-
tests/testthat.Rcorre ahora con el reporte del check y uno de progreso, asi que eltestthat.Routdel.Rchecktrae una linea por archivo con su tiempo:v | 178 | afirmaciones [3.0s] v | 2 54 | normalizacionPor que importa: el reporte del check no imprime nada hasta el final, y el 2026-09-26 el check de win-builder en R-devel murio con «Check process probably crashed or hung up for 20 minutes … killed» mientras las pruebas seguian corriendo. Una linea por archivo distingue «esta trabajando» de «se colgo», y de paso deja el reparto del tiempo por archivo en cada corrida de cada plataforma, que es lo que hacia falta para saber donde recortar.
Que el reporte del check siga haciendo fallar al check no se da por hecho: se comprueba corriendo
R CMD checksobre una copia del paquete con una prueba que debe fallar -daStatus: 1 ERROR- y otra vez sin ella. Un reporte de progreso que se comiera los fallos seria mucho peor que una suite lenta.
El enmascarado de la salida agrupa las hojas
-
Proteger una salida recorria cada hoja de texto por separado y pagaba en cada una el bucle completo sobre los valores protegidos: un
gsubpor valor, que cuesta lo mismo sobre una cadena que sobre diez mil. Medido, publicar 4.754 hallazgos costaba 42.794 llamadas al reemplazo con una mediana de un solo texto por llamada, y la culpable erahallazgos$trazabilidad, una columna-lista con una entrada por hallazgo y nueve hojas en cada entrada.Ahora el recorrido se hace en dos pasadas -cosechar las hojas, reemplazar una vez sobre el vector entero, repartir- y el mismo caso hace 2 llamadas. La llamada que tardaba 158 s tarda 11 s, con los mismos 6.204 pares publicados.
El recorrido es el MISMO en las dos pasadas, asi que los atributos anidados se siguen protegiendo igual y las hojas se visitan en el mismo orden. Que la salida no cambie no se da por hecho: la prueba la compara contra una implementacion de referencia hoja por hoja, escrita en el propio archivo de prueba, sobre un perfil, un plan, una columna-lista y un objeto con atributos anidados.
La otra mitad de la prueba fija la propiedad que lo hace rapido: la cantidad de llamadas no crece con la cantidad de hojas. Con cuarenta veces mas entradas, el mismo numero de llamadas. No se mide tiempo: un umbral de segundos medido en una maquina no transfiere a otra.
La cola del valor protegido mas largo ya no se publica
-
El enmascarado de la salida sustituia los valores protegidos uno por uno, en el orden de la lista, y un valor que es PREFIJO de otro dejaba publicada la cola del largo. Medido con
c("Maria Nunez", "Maria Nunez de Castro")sobre el texto"beneficiaria: Maria Nunez de Castro":antes: beneficiaria: [valor protegido] de Castro ahora: beneficiaria: [valor protegido]Los nueve caracteres que quedaban afuera pasan el piso con el que el propio paquete decide que un valor identifica -seis-, asi que era una fuga con la regla de casa. Y no era un caso de laboratorio: los valores salen de las celdas de las columnas protegidas en el orden de las filas, de modo que cual llegaba primero dependia de como estuviera ordenada la tabla. Ahora se sustituyen de mas largo a mas corto y el resultado no depende del orden de la lista.
Un valor repetido en esa lista se sustituia dos veces, y si el valor aparece dentro del marcador la segunda pasada lo corrompia: con
prot,dato [valor protegido]se volviadato [valor [valor protegido]egido], conegidopublicado afuera. Se sustituye una sola vez cada valor.La prueba que sostiene esto incluye una guarda que no cubre el defecto arreglado sino el arreglo que no se hizo: reemplazar el bucle por una alternancia de expresion regular es la optimizacion obvia -el bucle cuesta cientos de
gsubpor llamada- y, medida, dejaba de enmascarar el texto marcadolatin1, porque armar el patron traduce a UTF-8 y entonces una letra acentuada tiene dos bytes en el patron y uno en el sujeto. La prueba fija el enmascarado sobre textolatin1, sobre bytes que no son UTF-8 valido y sobre UTF-8, para que esa regresion no pueda entrar en silencio.
Un contrato incompleto se abstiene, no se lleva la corrida
?contratos_medicionpromete que «cada metrica valida los campos que necesita y se abstiene si faltan». Las cuatro metricas ligadas avigencia()-OportunidadEntPorFecha,OportunidadEntPorIntervalo,DesactualizacionPorFechayDesactualizacionPorCambios- llamabanstop(), ymedir()abortaba entero: sobre un modelo de dos metricas, una sin su campo se llevaba puesta la medicion de la otra y no quedaba ni un objeto que mirar. Ahora se abstienen: la medicion trae las demas medidas ycobertura_metricaspublica una fila con estadocontrato_incompleto, el campo que falta y como declararlo.evaluar()deja el resumen enNA, que es lo que ya hacia con cualquier metrica no medida.Mezclar una fecha de calendario con un instante cambia el veredicto sin que se vea: un
Datese ancla a la medianoche UTC y unPOSIXctvale por su instante, asi que una actualizacion del dia del limite puede contar como tarde, y si elPOSIXctse leyo sin huso el resultado depende de la sesion. Medido con los mismos datos y el mismo contrato:1 0 0conTZ=UTCy0 0 0conTZ=America/Montevideo. Ahora las cuatro metricas avisan cuando la columna y el campo del contrato que comparan no son de la misma clase, y explican como dejar el numero estable. Cuando las dos puntas son de la misma clase no avisa nada.La impresion de la medicion declara ahora las metricas que no se pudieron medir, con su estado y su motivo. Era la cuarta capa con la misma pregunta -el tablero ya imprimia esa tabla, el historico la registra como fila y
evaluar()dejaNA- y la unica que quedaba muda, justo la que se mira primero despues de medir: sobre un modelo de dos metricas se leian las dos medidas de una y nada decia que la otra no habia medido. Y cuando ninguna midio -un caso que antes no existia, porquemedir()abortaba-, lo unico que se imprimia era el encabezado de un cuadro vacio.-
Y el aviso no era de cuatro metricas sino de una propiedad: comparar temporales de clases distintas. Un recorrido de los catalogos -en vez de una revision de los archivos que ya conocia- encontro once sitios mas, cada uno medido contra su control. Las cuatro gemelas por atributo -
OportunidadAtributoPorFecha,OportunidadAtributoPorIntervaloy las dos deGradoOportunidadAtributo*- comparaban igual y callaban: medido con una columnaDatey unfecha_limitePOSIXctleido sin huso, la fila que cae exactamente en el limite daba1 1 0conTZ=UTCy1 0 0conTZ=Asia/Tokyo, sin un aviso. Y un recorrido del catalogo -en vez de una revision de los archivos que ya conocia- encontro una novena, que no tiene nada que ver con la frescura: las comparaciones POR CONJUNTO, que no tienen nada que ver con la frescura y fallan mas fuerte todavia, porque ahi la mezcla no corre el numero: lo vacia. Medido,0 0 0contra el1 1 0del control enValoresPosiblesPorExtension-dominio-,ValoresPosiblesPorComprension-rango-,Formato-diccionario- y las tres metricas referenciales -clave del padron-, donde la cobertura daba0en vez de1. Y el peor de todos:NoNulocon un centinelaPOSIXctsobre una columnaDatecon1900-01-01como ausencia disfrazada publicaba1 1 1-no falta nada-, contra el1 0 1del mismo centinela declarado como fecha. Una metrica de completitud informando completitud perfecta sobre una columna llena de ausencias.Las quince avisan ahora, con la consecuencia escrita para lo que cada una hace. Y la prueba que lo sostiene recorre los catalogos, prueba cada combinacion de propiedades que los validadores aceptan y fija el resultado de cada par (metrica, propiedades, columna), incluidos los pares donde la guarda calla a proposito: una columna numerica contra una declaracion temporal no es la confusion entre calendario e instante. Una metrica nueva que compare fechas aparece como un par que no esta en la tabla y la prueba falla.
La semantica publicada de
OportunidadEntPorFechadecia «antes de su fecha limite» y el calculo es<=: la fila exactamente en el limite salia oportuna. La gemela por atributo ya declaraba «hasta la fecha limite inclusive» y el catalogo dice «entrega hasta Tf, inclusive», asi que la conducta era la correcta -entregar el dia del plazo es a tiempo- y la descripcion la que mentia. Se corrigio la descripcion.
La metrica dice cuantas midio de cuantas
- Una metrica por celda mide solo las celdas con valor: sobre cuatro celdas con una ausente publica tres medidas, el agregado se calcula sobre esas tres y nada decia cuantas quedaron afuera. El extremo estaba cubierto -sin ningun valor, la metrica va a
cobertura_metricasconsin_valores- y el caso parcial pasaba en silencio: el tablero publicabavalor = 0.667conuniverso = "celdas", que no se distingue de un 0.667 sobre las cuatro. Ahora la medicion publicaalcance_medidas-cuantas midio, de cuantas y en que unidad-, viaja al tablero y se declara en las tres salidas: la impresion de la medicion, la del tablero y el informe HTML. Los dos estados siguen separados, porque son afirmaciones distintas: «no se pudo medir» y «se midio una parte».
La frontera y sus pesos hablan el mismo idioma, y el informe publica el hueco
La medicion agregada a
coleccionlleva la cobertura de esa coleccion como atributo y su impresion la tiraba: sobre tres tablas declaradas con una que no se pudo leer, la fila publicaentidad = tres_tablasy el promedio de dos, conadvertencia_agregacion = NA. El indice ya imprimia esa cobertura y el tablero se arreglo en su vuelta; esta era la tercera capa con la misma pregunta, y la primera que se mira despues de agregar.Los pesos de
promedio_ponderadose emparejaban solo contraobjeto_medible, que en los niveles altos es la lista de partes unida con coma -una construccion interna-. Asi que al agregar dos colecciones a una organizacion, los pesos escritosc(padron_a = 0.5, padron_b = 0.5)-los mismos nombres queorganizacion(colecciones = )exige- se rechazaban, y habia que escribirc("t1, t3" = 0.5, ...), que nadie declaro en ningun lado. Ahora se acepta cualquiera de las dos identidades, y el error entrecomilla cada parte: dos partes llamadast1, t3yu1, u2se leian como cuatro nombres separados por coma.El informe HTML publica ahora
cobertura_reglas: una regla que declara una metrica que la medicion no trae dejaba su hueco en el objeto y en la consola -desde la vuelta anterior- mientras el documento que se manda a otra persona publicabaresultado = 0.9y nada mas. El veredicto se leia como completo.
perfilar() no aborta sobre una columna con marca en el minimo declarado
-
.diagnosticar_texto()deduplicaba los ejemplos de espacios sobrantes conunique(), y esa operacion aborta en R 4.1 -el minimo que declara elDESCRIPTION- sobre una cadena marcadabytes:perfilar()se caia sobre una columnalatin1valida. Ahora deduplica por la clave de bytes. Y el barrido que lo comprueba no mira ese sitio: pasa toda la API publica -perfilar, analizar, planificar, guiar, aplicar, reportar, perfilar_por, duplicados, patrones, formatos, deriva- por una tabla cuyas columnas de texto llevan la marca, dentro del contenedor del minimo.
La regla declara mas de lo que el veredicto cubre, y el historico lo respeta
Una regla de evaluacion declara las metricas que evalua. Si alguna no tiene medidas en la medicion, el veredicto cubre menos de lo que la regla dice: medido, una regla sobre dos metricas -una nunca instanciada- publicaba
n_medidas = 3, resultado = 1, identico a la regla que declara solo la que existe, mientras el propio objeto conservaba en su configuracion que la regla declaraba dos. La maquinaria del silencio declarado existia para el borde -si NINGUNA coincide,evaluar()se niega nombrando solicitadas y disponibles- y la coincidencia parcial pasaba sin nombrar nada. Ahora se avisa al evaluar, queda enattr(evaluacion, "cobertura_reglas")con su motivo y la impresion lo declara.desenlace = "suprimir"declara que una medida no debe publicarse, y la impresion y el informe lo sostienen.historico_calidad(ev, detalle = "completo")-la tabla que la documentacion presenta como exportable conwrite.csv()- publicaba su valor: dos salidas del mismo objeto con politicas opuestas sobre la misma medida, y la que se exporta era la que no la respetaba. Ahora esa fila dejaresultadoenNAy marcaobjeto_mediblecon[valor suprimido], que es la convencion que esta tabla ya usaba para el nivelmedida; el objeto guardado sigue conservando el valor, como esta documentado.
La medicion de la perdida no puede abortar en el minimo declarado
Medir la perdida en la puerta comun -la vuelta anterior- llevo la comparacion de celdas a un camino nuevo:
aplicar()sobre una columna marcadabytesabortaba en R 4.1 -el minimo que declara elDESCRIPTION- con “translating strings with bytes encoding is not allowed”, porque.celdas_cambiadas()compara con!=y esa version no traduce una cadena marcada. En 4.6 no aborta, asi que la suite local no lo veia: lo encontro el contenedor del minimo. La comparacion pasa ahora por la clave de bytes, que es lo que el paquete ya usa para esto y no traduce nada.Dos pruebas del paquete pedian a R lo mismo que el paquete evita:
unique()y==sobre una cadena marcadabytes, que en 4.1 abortan. Ahora afirman siempre la cardinalidad medida y contrastan contraunique()de R cuando esa version lo permite, comprobandolo midiendo en vez de por numero de version, y el salto declara el motivo.
Lo que el perfil concluye, y quien puede declarar un dato personal
Un entero escrito como texto por encima de 2^53 publicaba el redondeo como si fuera el dato: sobre
9007199254740993/4/5-tres impares- el resumen deciaminimo = 9007199254740992ymaximo = 9007199254740996, dos valores que ninguna fila contiene, conestado_resumen_cuantitativo = "calculados". La misma columna guardada comointeger64recibia la respuesta correcta desde el primer dia -medidas enNA, extremos exactos yomitidos_precision-: un dato, dos caminos, dos respuestas. Ahora el camino del texto da la misma que el del entero, y las comparaciones se hacen por longitud y en orden de bytes, no con el<del locale.n_valores_excluidos_resumenpromete contar todo valor presente que no sostiene el resumen y que, siempre que ese total sea mayor que cero, el perfil agregue una filaresumen_cuantitativoa la cobertura. La fila se decidia por una lista de dos estados -los del resumen parcial-, asi que el resumen que no se pudo calcular quedaba sin declarar: tres fechas ambiguas publicaban 3 excluidos con la cobertura vacia. Ahora manda el numero, como dice la promesa, y el motivo distingue “no se pudo calcular” de “se calculo sobre N”.Una columna de periodos mensuales publicaba
NAen ese campo mientras contaba tres enn_fechas_excluidas_granularidad. Y una columna con tres fechas de dia ambiguas mas un periodo de mes publicabagranularidad_incompleta, un estado que la documentacion define para columnas expresadas solo como mes y anio: culpaba a la granularidad de un bloqueo que era de la ambiguedad dia/mes. Las dos cosas quedan medidas y nombradas.columnas_personaleses el mecanismo que el paquete ofrece para declarar personal una columna que su lexico no reconoce -una edad, un legajo interno-.medir()ya lo aceptaba;distribucion_valores(),clasificar_variables()ydetectar_discordancias()no, y sin perfil la clasificacion corre solo por lexico y por forma:distribucion_valores(datos)publicaba los veinte valores mas frecuentes y el maximo exacto de una columna queperfilar(columnas_personales = ...)enmascara. Las tres puertas aceptan ahora la declaracion, y sus paginas dicen que sin perfil ni declaracion la proteccion alcanza solo a lo que la forma delata.La pagina de
perfilar()describia un piso absoluto -“no se publica en ninguna parte”- yperfilar_por()publica las etiquetas de grupo cuando se agrupa por una columna personal, con aviso y con su atributo. Era la unica excepcion y no estaba nombrada ahi: ahora si.La misma pagina enumeraba
Date,POSIXtydifftimejunto ainteger64,unitsyPeriody prometia en la misma oracion una fila de cobertura para “las columnas que R declara numericas”.is.numeric()es FALSE para las tres primeras, asi que no la reciben -y no corresponde que la reciban: no son magnitudes para Benford, igual que una columna de texto-. El texto lo dice ahora sin que el lector tenga que probaris.numeric(Sys.Date()).La vinieta del plan de limpieza declara los tres huecos del plan y como se cuenta
n_no_reversibles.
La perdida se mide en la puerta comun
n_no_reversiblespromete contar las celdas cuyo valor se perdio, y la documentacion fija el criterio: se mide sobre el resultado, no por el nombre de la accion. No se medía. Los ejecutores que no devolvian la cuenta la dejaban en cero, asi queconvertir_mayusculas,convertir_minusculas,convertir_titulo,convertir_segun_diccionario,decodificar_entidades_html,reemplazar_separadoresyreparar_codificacionpublicaban “0 irreversibles” sobre columnas donde dos valores distintos habian quedado en uno. Medido sobreana | ANA | Ana | beto: la mayuscula funde dos celdas y el registro decia 0, mientraseliminar_controles_invisiblesdecia 1 con la misma fusion. Ahora la cuenta se toma en la puerta comun -comparando la columna antes y despues-, asi que alcanza tambien a la proxima estrategia que se agregue sin devolverla; las que si la devuelven conservan la suya, porque miden perdidas que no son fusion.aplicar()se niega cuando el plan trae una accion eliminatoria sinpermitir_eliminacion = TRUE, y esa negativa alcanza a todo el plan: las acciones benignas seleccionadas tampoco corren, y no hay registro porque no hubo ejecucion. El mensaje decia solo que hacia falta el consentimiento. Ahora dice que no se aplico nada, que los datos quedaron intactos y cuales son los dos caminos; la pagina deaplicar()lo declara igual.Un
perfil_dbitrae dos coberturas y su impresion nombraba una. La otra -que diagnosticos no se evaluaron sobre la muestra- quedaba invisible, porquecobertura()sobre ese objeto devuelve la de metricas SQL. La impresion la declara ahora con su cuenta y su ruta, y la pagina de los accesores dice cual devuelve cada uno.detectar_duplicados_aproximados(normalizar = FALSE)compara igual las formas canonicamente descompuestas -es lo que hace que dos escrituras del mismo texto sean el mismo texto-, y eso cambia la distancia:cafecon tilde ycafedistan 0.04 descompuestas y 0.117 tal como se guardaron, asi que el par entra o no en un umbral de 0.1 segun cual se mida. La conducta es la correcta y no cambia; lo que faltaba era decirlo en la pagina de la funcion, que enumeraba los pasos configurables y callaba el que siempre corre. Una prueba fija que la cifra publicada se reproduzca desde las formas descompuestas.La misma funcion mide la distancia sobre los valores concatenados, asi que el orden en que se combinan las columnas cambia el resultado: sobre las mismas cinco filas,
c("nombre", "domicilio")publica 6 pares yc("domicilio", "nombre")publica 10, con el mismo umbral. Declararcolumnasvuelve el resultado independiente de como esten ordenadas en el archivo -y sin declararlas manda ese orden-; el objeto publica el vector que uso, asi que la corrida se puede rehacer. Ahora lo dice la pagina de la funcion, que tambien declara que una fila sin texto comparable -todos sus valores ausentes o vacios- no entra en la comparacion, mientras una cadena vacia si es clave valida enbloquear_por.
El plan declara los tres huecos, no dos
El plan promete declarar lo que no cubre y declaraba dos huecos: el diagnostico que el perfil no pudo evaluar y el hallazgo cuya columna comparte nombre con otra. Faltaba el tercero, el mas silencioso: el hallazgo que el perfil midio, con su columna identificada, que ninguna accion del plan atiende. Medido sobre las dos tablas del paquete y dos fixtures:
patron_raro,casi_duplicados_vocabularioyrelacion_orden_columnasno tienen estrategia en el plan, yfaltantesytipo_declarado_distintotienen una que puede no proponer nada. Leer el plan se leia como “no hay nada que hacer ahi”. Ahora viajan enattr(plan, "hallazgos_sin_accion")con su motivo medido -si la misma columna recibio otras acciones, lo dice, sin afirmar que alguna cubra el hallazgo-, la impresion los anuncia y el informe HTML los publica en su propia seccion.Ese atributo copia la sugerencia del hallazgo, asi que pasa por la misma proteccion de datos personales que el resto del plan: un atributo nuevo no puede ser la puerta por la que sale un valor que las columnas ya no publican.
El marco que se publica contiene lo que se publica
El tablero publicaba una fila con valor y, al lado, un alcance que decia “se midieron 0 factores de este marco”. Las dos cosas eran ciertas: la cobertura se cuenta sobre los factores del marco declarado, y una medida cuyo par dimension-factor ese marco no declara no entra en ninguna casilla -medir con las metricas del paquete e informar contra ISO/IEC 25012, por ejemplo-. Juntas se leen como una contradiccion del objeto. Ahora el alcance publica
medidos_fuera_del_marco, que no suma con las otras cuatro casillas, el atributopares_fuera_del_marcolos nombra y la impresion lo dice. Informar contra el marco propio sigue siendo posible: es el flujo de la vinieta de inicio, donde la cobertura del marco se satisface porperfil_mide.El catalogo de AGESIC publica en
implementacionuna receta, no una expresion que se evalue sola. La documentacion declara ahora sus dos unicos nombres libres -m, la medicion ya tomada de la metrica base, yu, el umbral que elige quien agrega- y una prueba exige que no aparezca un tercero y que la receta, con esos dos ligados, devuelva el ratio que nombra.
Un recorte dice cuantos son, y cada numero dice en que unidad cuenta
guiar_limpieza()mostraba cinco ejemplos de cuarenta sin decir que eran cinco de cuarenta, justo antes de que alguien decida si aplica la accion. Cada recorte viaja ahora con su total y la pantalla lo declara.Las acciones informativas -
revisar_cardinalidad,revisar_ceros,revisar_negativos- publicaban la cantidad de filas de la columna junto a la unidadcolumnaque heredan del hallazgo: sobre una tabla de 80 filas y 7 columnas se leian_afectadas = 80conunidad_conteo = "columna". Ninguna lectura salvaba ese par. Ahora publican el alcance que midio el hallazgo que las origino.
El minimo declarado se comporta como el declarado
En R 4.1 -el minimo que declara el
DESCRIPTION- una celda marcadabytesdejaba de estarlo al normalizar:trimws()ygsub(fixed = TRUE, useBytes = TRUE)devuelven ahi la cadena sin marca, mientras en versiones recientes la conservan. La marca se vuelve a poner en vez de confiar en la operacion.Por la misma via, la cuenta de celdas que pierden valor compara claves por bytes: en esa version
duplicated()ysplit()sobre una cadena marcada abortan con “translating strings with bytes encoding is not allowed”.
La capa de modelado declara sus limites
-
comparar_equivalencia()publicano_comparablecuando un campo no tiene valor medible en ambos lados o solo lo tiene en uno, y lo cuenta separado deidentico. -
perfiles_madurez()no juzga contra [0, 1] una medida que no sea una proporcion segun su tipo, su unidad o sus valores observados. -
agregar()deja visible cuandoconjuntoEntidadeshace un promedio sin pesos y no conoce el alcance de sus partes. Desdecoleccionsiguen siendo obligatorios los pesos porque la frontera declarada exige decidir cuanto aporta cada tabla. -
granularidades()distingue que un nivel sea medible de que tenga una transicion de agregacion.conjuntoAtributosconservaimplementada = TRUE, pero declara que no es agregable con el grafo disponible.
Lo que se publica y lo que se declara proteger
La etiqueta de una columna personal protegida decia
[estadisticos de orden y momentos protegidos]y publicaba un momento: el desvio. La decision de conservarlo esta tomada -una dispersion no identifica a nadie y la media si se enmascara-, asi que lo que se corrigio es la etiqueta, que ahora dice[estadisticos de orden y la media protegidos]. Viaja ademas una prueba que no lee la lista de campos a ocultar: perfila una columna protegida con magnitudes conocidas y exige que ningun campo publicado las traiga, con el desvio como unica excepcion declarada.desenlace = "suprimir"declara que una medida no debe publicarse, y el informe enmascaraba solo suvalor_medido: dos secciones mas abajo, la tabla de medidas publicaba su resultado. Ahora el resultado tambien se enmascara al publicar -en el informe y en la impresion-, y el objeto guardado no cambia.Los metodos
printdel perfil y del plan declaran cuantos campos muestran de los que el objeto tiene, y donde estan los demas. El del plan ocultaba en silencioevidenciayjustificacion, que son lo que se lee para decidir si aplicar una accion.
La perdida se mide en el resultado
-
n_no_reversiblescontaba por accion: toda celda que la accion tocaba, o ninguna. Ahora una celda cuenta como perdida cuando la transformacion la deja indistinguible de otra que era distinta, o cuando la deja ausente. Quitar un guion suave dePRO<U+00AD>DUCTO-Adeja el valor en su forma canonica y no cuenta; quitar un espacio de ancho cero que separaba dos claves las fusiona y si cuenta.recortar_espaciostenia el defecto simetrico: dejaba" ana "y"ana"en un solo valor y publicaba cero irreversibles.
Colecciones: alcance y trazabilidad de las lecturas
La poda cierta por rangos en
relaciones_coleccion()usaMINyMAXdel universo completo de cada tabla. Un rango calculado solo sobre la muestra ya no puede publicarsin_coincidencias; si el motor no entrega el agregado, el par se compara sin esa poda.Cada fila de
resumen_colecciondeclara eluniversode las metricas que alimentan sus agregados, para no confundir un conteo de la tabla completa con proporciones observadas sobremuestra_motor.Las divergencias de corroboracion y el SQL de cada tabla se conservan al subir un perfil a una coleccion, aun cuando
conservar_perfiles = FALSE.n_filas()devuelve el vector de conteos por tabla para una coleccion;sql_perfil()devuelve la tabla de lecturas acumuladas.El resumen de la coleccion dejo de atribuirle al motor un recorte que puede venir del presupuesto declarado por quien llama: dice cuantos agregados no se calcularon y, cuando el motivo es uno solo, cual es. Con
max_consultas = 3ninguna consulta la rechaza el motor.-
estimar_costo()declara la corrida que esta estimando:modo_comparaciones el que publicaradetectar_duplicados_aproximados()con esos argumentos, yfilas_previstas,filas_totalesyestrategia_previstadicen sobre cuantas filas se va a comparar. Antes afirmabaexhaustiva_por_bloquessiempre, y con los mismos argumentos la corrida podia comparar 447 filas de La corroboracion entre los dos bloques de
perfilar_dbi()ya no se calla cuando la muestra es parcial: las diferencias que no se declaran divergencia -porque una submuestra difiere por muestreo- quedan en una fila de coberturano_corroboradaque dice cuantas son y sobre que fraccion de filas.La clasificacion de datos personales publica
proporcion_verificada, la proporcion de valores que paso el validador y que decidepoder_discriminante = "verificado"contra su umbral. Una columna con el 90 % de las cedulas validas -el umbral- publicaba lo mismo que una con el 100 %.
Columnas con clase propia y silencios del plan
perfilar()ya no aborta sobre una columna cuya clase respondeTRUEais.numeric()pero no hace la aritmetica de un doble. Las relaciones aritmeticas y la ley de Benford se buscan ahora entre columnas numericas sin clase declarada, en vez de excluir una lista fija de clases, y cada columna numerica que queda afuera por su clase se declara encobertura_diagnosticoscon su clase y con como evaluarla.meta$aritmetica_columnaspublica las clases medidas en la tabla.Contar filas duplicadas sobre una tabla que combina una columna con dimensiones y una columna de listas ya no aborta: el aplanado de columnas compuestas esta escrito una sola vez y arma la tabla sin coaccionar las listas.
Sobre una columna factor, las acciones de limpieza por celda declaran en su justificacion que devuelven texto y que el orden y los niveles sin observaciones no se conservan. Si el factor es ordenado, la accion queda recomendada pero sin activar.
El plan declara en
hallazgos_sin_accion_por_columna_ambigualos hallazgos medidos para los que no propone accion porque su columna comparte nombre con otra, y lo avisa al imprimirse.Los valores presentes que no son finitos -
NaN,Inf,-Inf- cuentan como excluidos del resumen cuantitativo, como los dos README ya prometian:n_valores_excluidos_resumenlos suma, el estado pasa acalculados_sobre_valoresycobertura_diagnosticosrecibe su fila con el desglose. Una columna sin un solo valor utilizable publicasin_valoresen vez decalculadoscon todo enNA.El denominador de
outliers,ceros_no_permitidosynegativos_no_permitidosdescuenta tambien los ausentes comunes, no solo los valores que no pudieron convertirse: una columna de mil filas con cincuentaNApublican_evaluados = 950.eliminar_controles_invisiblesdeclara en su justificacion que quitar el caracter no deja forma de reconstruir la celda y que el registro cuenta cada una enn_no_reversibles. La documentacion deplanificar_limpieza()precisa quedestructivamarca las acciones que hay que activar a mano y no equivale a “no se perdio nada”.
Correcciones de marco y deriva
medir()conserva el marco declarado por el modelo ytablero_calidad()lo reutiliza para publicar la cobertura completa. El historico guarda tambien el nombre y los pares dimension-factor del marco, junto con eltipo_resultadode cada metrica. Si cambia el marco o un tipo, la deriva lo declarano_comparablecon severidaderrory explica el motivo.La deriva ya no declara resuelto un hallazgo que cuenta filas de la tabla si cambiaron las columnas: publica
no_comparablecon severidaderrory nombra las columnas aparecidas o desaparecidas. La documentacion demarcar_filas_duplicadasaclara que la marca no elimina filas y que sus columnas hacen que un perfil posterior no vuelva a contarlas como duplicadas.Un analisis releido declara en sus advertencias y junto a la propuesta que esta se volvio a derivar sin las funciones de reglas sustituidas, por lo que puede no coincidir con la decision original, que se conserva.
Correcciones de cobertura y publicación
Comparar al revés ya no publica la dirección al revés.
comparar_perfiles()ycomparar_evaluaciones()publican en cada fila la fecha de cada corrida, pero no las miraban: con los argumentos cambiados, un hallazgo que se agravó salíaatenuado, un patrón que apareció salíadesaparecidoy un delta que subió salía negativo, contradiciendo a las fechas de la propia fila. Ahora paran con un error que nombra las dos fechas y dice que hay que invertir los argumentos. La exigencia corre sólo cuando las dos fechas las declaró quien llama —perfilar(fecha = ),medir(fecha = )—, que es como se construye una serie: la fecha por omisión es la hora de la corrida, y dos perfiles hechos en la misma sesión no son una serie. El perfil publica enmeta$fecha_declaradacuál de los dos casos es.Un hallazgo editado a mano no se publica en silencio. La guarda que compara lo que un hallazgo afirma con las filas que lo respaldan corría al construir el perfil y no al publicarlo: un objeto con
n_afectadoscambiado a mano salía porhallazgos()y por el informe sin que nada dijera que la cifra y su respaldo no coinciden. Ahora la guarda corre también en esas dos puertas.La clasificación personal DBI declara su alcance. Si el perfil de muestra queda recortado, los valores del resumen SQL completo se marcan en
resumen_tabla$cobertura, se protegen con la politica activa y no se repiten en la corroboracion como si la muestra hubiera cubierto toda la tabla. Con una muestra completa no cambia ninguna cifra.perfilar_por()documenta quemuestraacota dentro de cada grupo. Los argumentos que acotan el trabajo viajan a cada grupo, así que conmuestra = 100y grupos de 500 filas los diagnósticos que muestrean miran 100 de cada grupo. El alcance ya viajaba en la evidencia de cada hallazgo; lo que faltaba era decirlo donde se lee la función.El motivo publicado distingue presupuesto de capacidad en las sondas DBI. Si
max_consultasbloquea una sonda, la cobertura conserva el mensaje del tope declarado; si el motor rechaza la consulta, se mantiene el motivo de capacidad.Los índices de un hallazgo por grupo apuntan a la tabla del usuario.
perfilar_por()perfila cada grupo sobre su rebanada y publicaba los índices de esa rebanada conlocalizador = "indice_fila"yalcance = "completo": contra la tabla original señalaban filas inocentes. Ahora se traducen al marco de la tabla que se pasó, que es el que la documentación promete.Un análisis guardado sin datos ya no dice que los conserva.
guardar_analisis(incluir_datos = FALSE)dejabadatos_conservados = TRUE—el valor de cuando se creó el análisis— y el informe del objeto releído publicaba «datos conservados: TRUE» sobre un objeto cuyodatosesNULL.Lo indeterminado deja de contarse como «no corresponde». La documentación de
aplicabilidadpromete que las filas donde el predicado no se puede determinar se declaran aparte; se contaban además como valores presentes fuera del universo y disparaban el hallazgovalor_fuera_de_aplicabilidad, cuya descripción —«la regla declarada dice que no corresponde»— es falsa para una fila que no se pudo decidir. Ahora esas filas se declaran encobertura_diagnosticosy la columna publican_presentes_en_aplicabilidad_indeterminada. El mismo hallazgo publicaba su universo al revés —«1000 afectados de 0 evaluados» con un universo aplicable vacío—: su denominador es ahora el de las filas que podían producirlo.La equivalencia declara con qué escala decidió.
comparar_equivalencia()publicaba una columna llamadadiferencia_relativaque por debajo de magnitud 1 no es relativa: la tolerancia es mixta —la misma que usan las relaciones aritméticas— y ahí el divisor es 1, así que una media de0,001que pasa a0,4salíaequivalentecon tolerancia0,5. El criterio se mantiene, porque dividir por casi cero convierte el ruido en un cambio del cien por ciento; lo que cambia es que ya no viaja callado: la columna se llamadiferencia_normalizada, elmotivodice cuándo la escala fue la unidad y la documentación lo explica con ese caso.La evidencia de Benford publica su desglose. Imprimía
1:NA/NA; … 9:NA/NAen todas las corridas, al lado de un chi-cuadrado con su p-valor: pasaba las nueve proporciones juntas a un formateador que devolvíaNApara cualquier vector. Los dos formateadores de publicación ahora devuelven un vector cuando reciben un vector, así que ninguna evidencia vuelve a quedarse sin números sin que nadie se entere.Lo que no se puede medir ya no se publica como medido. Tukey declara un IQR cero o menos de 20 valores en la cobertura; las asociaciones conservan los pares sin filas completas o con medidas no finitas; y
perfilar_por()mueve a cobertura los centinelas que solo aparecen por partir una columnainteger64. Las acciones de marcar o eliminar ausentes respetan el universo declarado poraplicabilidady fallan sin tocar datos si no pueden evaluarlo.Los enteros anchos que llegan como doble ya no se publican como exactos.
perfilar_dbi()reconoce BIGINT y tipos decimales de escala cero con precision potencialmente peligrosa. Si el driver entrega un valor de al menos 2^53 como doble,resumen_tabladeja las metricas de magnitud enno_disponible. La muestra trae esas columnas como texto y las convierte ainteger64cuandobit64esta disponible, para que los distintos y los diagnosticos de identidad sean exactos; sinbit64quedan como texto y la cobertura publica el remedio. Los estadisticos de magnitud de la muestra usanomitidos_precision. Pedirinteger64al conectar tambien conserva la medicion exacta. Losdouble,realyfloatdeclarados siguen tratandose como dobles.El texto del usuario ya no se ejecuta al imprimirse. Los mensajes de consola pasaban por la plantilla de cli, que evalúa todo
{...}, y el paquete la armaba con texto del usuario:perfilar(d, nombre = "tabla{1+1}")se imprimíatabla2, y una columna llamadatasa{Sys.getenv("USER")}se imprimía con el nombre del usuario al mostrar el plan. El encabezado de un CSV podía ejecutar código en la sesión de quien lo analizaba. Ahora ese texto se publica literal, y una prueba recorre todas las funciones del paquete para que ningún mensaje nuevo vuelva a armarse así. De paso, un nombre de columna declaradobytesya no aborta la impresión del plan ni deja escapada la frase del paquete que lo rodea.Las puertas que no aceptan un perfil de base de datos dicen qué hacer.
reportar()recorría el objeto deperfilar_dbi()como una lista y terminaba culpando a alguno de sus componentes;comparar_perfiles()pedía «un objeto producido porperfilar()». Ahora los dos nombran el camino: el perfil está en$perfil_muestra, y el resumen SQL de la tabla completa no es un perfil.Lo que se cuenta sobre la muestra dice que es de la muestra. Patrones y formatos de fecha se descubren sobre una muestra, pero sus hallazgos publicaban los conteos como cifras de la tabla —
n_afectados = 2donde la tabla tenía 10,%d/%m/%Y (50)donde tenía 5.000—, y la consola imprimía el veredicto sin mencionar la muestra. Ahora la evidencia lo declara, igual que ya lo hacíatipo_declarado_distinto;print()del perfil dice sobre cuántas filas trabajó, y el reporte anota la muestra también en los formatos de fecha.Lo que la muestra fabrica ya no se publica como propiedad de la tabla. Las dependencias se buscan sobre una muestra sistemática, y los descartes por casi-clave se decidían sobre esa muestra: con los datos agrupados en filas consecutivas, la muestra toma una fila por grupo, el determinante parece una clave y la dependencia desaparece. La tabla vacía se leía como «no hay dependencias». Ahora los descartes se verifican sobre la columna completa, y si sólo la muestra los produjo el motivo lo dice (
casi_clave_solo_en_muestra) y el perfil lo declara encobertura_diagnosticos.Una columna constante de fecha-hora ya no acusa al paquete. La traza del hallazgo
constantecomparaba el texto del vector con el de la moda, que en fecha-hora lleva la zona: salía vacía y el perfil avisaba «Es un problema delupa» sobre datos corrientes.reportar()ya no aborta con texto sin marca que no es UTF-8, el que dejaread.csv()sinfileEncodingsobre un archivo latin1. Se publica como lo muestra la consola, igual que el texto declaradobytes; y un nombre de columna así se cita igual en la evidencia, tenga o no la marca.La aplicabilidad declarada llega a los diagnósticos de texto y a la limpieza.
perfilar(..., aplicabilidad = )recortaba el universo de los faltantes, pero los diagnósticos de texto seguían mirando la columna entera — el paquete llegaba a avisar que un hallazgo contaba 6 y trazaba 4—, y las acciones de limpieza modificaban también las filas declaradas fuera: unaS/Den una fila donde la columna no corresponde se volvía ausencia. Ahora el perfil guarda la regla y [aplicar()] no toca las celdas de fuera. Las conversiones de tipo, que no se pueden aplicar a media columna, lo declaran en el plan.Una transformación de texto ya no depende de lo que haya en el resto de la columna. Un solo valor declarado
byteshacía que reemplazar separadores dejara ilegibles las celdaslatin1vecinas, y que pasar a minúsculas o mayúsculas abortara. Lo mismo pasaba con el texto sin marca que no es UTF-8 —el que dejaread.csv()sinfileEncodingsobre un archivo latin1—: recortar espacios, una acción recomendada y aplicada sola, fallaba. Esas celdas quedan ahora intactas, con su marca —el perfil no las mide y las informa como codificación inválida—, y la acción transforma las demás. Lo mismo vale para las declaradasbytesque no decodifican, que el recorte alcanzaba igual: el plan estimaba 1 y el registro contaba 3.La conversión de números con formato regional dice lo que hace y cuenta lo que cambia. El plan estimaba sólo los valores con separadores y el registro contaba todos —11 contra 20 en la misma columna—; ahora los dos cuentan valores presentes, como las otras conversiones de tipo. Una columna de
"10%"pasaba a0.1sin decirlo: la justificación declara ahora la división por 100 y la unidad o el símbolo de moneda que dejan de formar parte del valor. Y una columna que no se convierte publica su motivo real, en vez de atribuirlo siempre a la ambigüedad de los separadores.La conversión de marcadores de ausencia convierte exactamente lo que el hallazgo declara. La acción usaba su propio catálogo y borraba además códigos que la detección había dejado fuera a propósito —
SD,NCyND, que en una columna de estados de EE.UU. son Dakota del Sur, Carolina del Norte y Dakota del Norte—. Y abortaba si la columna tenía un solo valor declaradobytes.La comprobación de pérdida de precisión reconoce cualquier formato que el conversor sepa leer, incluidos la notación científica y los números con coma decimal, en vez de una lista de formatos que se quedaba corta.
El plan ya no recomienda acciones que no se pueden ejecutar. Marcar filas duplicadas sobre una tabla con columnas de lista queda bloqueado con el motivo, en vez de recomendarse y fallar en cada corrida.
La deriva ya no declara resuelto lo que el perfil nuevo no evaluó por falta de un paquete opcional en la geometría (validez y dominio de coordenadas), y las acciones que destruyen valores —centinelas numéricos, winsorización— cuentan sus pérdidas en el registro.
Una conversión que redondea ya no se declara reversible.
convertir_numero_regionalconvertía9007199254740993en9007199254740992—un entero por encima de 2^53 no entra en un número de doble precisión— y el plan la publicaba recomendada, reversible y con cero cambios irreversibles. Ahora la pérdida se mide al parsear el número, antes de aplicar cualquier unidad o moneda, y la acción queda para activar a mano con el motivo escrito. Lo mismo vale para los decimales, que la comprobación no miraba:33.333333333333333333%tiene veinte cifras y un número de doble precisión guarda diecisiete. Un decimal se compara con la precisión con que vino escrito, así que una columna común con decimales no se acusa.Toda conversión que pierde valores se marca
destructiva. La marca se calculaba sólo cuando la columna era segura: en una que mezcla10%con0.1, el plan medía cinco valores irrecuperables y decía «Se declara destructiva» en su motivo, pero la marca quedaba enFALSE. Activada a mano, el aviso de acciones destructivas no aparecía y el registro repetíaFALSE.Un marcador de ausencia que podría ser un dato ya no se convierte solo.
NAes el código de Namibia yNULLpuede ser un apellido: la acción que los convierte en ausencias queda para activar a mano cuando el marcador es una palabra suelta. Los que llevan puntuación o espacios —S/D,sin dato— se siguen aplicando solos.La deriva distingue lo que se resolvió de lo que se dejó de mirar al cambiar el tipo. Cuando una columna pasa de texto a número, los casi-duplicados de vocabulario dejan de evaluarse; antes se informaban como resueltos, y ahora como no evaluados.
Una acción recomendada ya no destruye el valor del usuario.
eliminar_controles_invisiblesynormalizar_espacios_invisiblesreemplazaban el valor por la cadena literal"NA"cuando no podían decodificarlo: sobre una columna que mezclalatin1con UTF-8, tres de siete valores. Yrecortar_espaciosconvertía textolatin1válido en bytes inválidos, porquetrimws()sobre un vector que contiene una sola cadena marcadabytesdevuelve marcadasbytestambién a las que viajaban al lado. El perfil siguiente acusaba entonces una codificación rota en las filas que la limpieza acababa de tocar. Ahora lo que no se puede leer se deja intacto, y cada grupo de marca se transforma por separado.El perfil ya no depende de si la columna es
factorocharacter. Los mismos datos daban tablas de patrones distintas: como factor, las filas del valor declarado desaparecían y la proporción del patrón restante se publicaba como1.000sobre el 75 % de la columna.La evidencia dice cuándo escapó los valores. Las tablas del perfil muestran el valor y la evidencia lo escapa para que el defecto se vea —un espacio duro publicado crudo se ve como un espacio común—, y ahora el hallazgo declara cuál convención usó, para que nadie busque en sus datos un
<U+00A0>literal. La deriva, por lo mismo, cita el valor cuando la diferencia está sólo en los espacios del borde: antes informaba un cambio con severidaderrorcuyos valores anterior y actual se imprimían idénticos.La evidencia de un hallazgo publica el valor, no la forma con que se lo analizó. Comparar vocabulario exige interpretar el texto, y un valor declarado
bytesse marca UTF-8 antes de compararlo. Pero lo que se informaba salía de esa forma interpretada, así que el mismo valor aparecía de dos maneras dentro del mismo objeto: la tabla del perfil publicabani\xc3\xb1oy la evidencia publicaba la eñe armada. Quien leía el informe no podía saber que eran el mismo valor, que es justo lo que el hallazgo le pide mirar antes de unificar. Dos fronteras quedan explícitas:latin1no esbytes—ahí R conoce la codificación y la convierte sin pérdida—, y cuando una forma junta secuencias de bytes distintas que se interpretan igual, la etiqueta vuelve a la unidad en la que se contó, porque decirni\xc3\xb1o (3)afirmaría que esa secuencia aparece tres veces cuando aparece una.La remediación de capitalización opera sobre caracteres, y la deriva informa el patrón y no su clave.
mayusculas/minusculasrecibían el texto sin marcar, así que sobre un valor declaradobytestoupper()no tenía cómo saber que eran caracteres y trabajaba sobre octetos sueltos. Yderiva_perfil()publicaba la clave interna del patrón —escapada byte a byte— en la columna donde el lector espera el patrón.El plan de limpieza ya no depende de cómo estén ordenadas sus filas. Dos acciones con el mismo
ordendesempataban por la posición de la fila, así que el mismo plan con las filas invertidas devolvía datos distintos: una corrida conservaba un espacio inicial y la otra no. Ahora el empate lo resuelveid_accion, que no cambia al reordenar. Y un grupo marcado comoelegidasin ninguna acción activa se rechaza con un mensaje que dice qué hacer, en vez de informar una elección que no eligió nada.Una distribución que no pudo analizar todos los valores ya no se declara completa. Las proporciones de
distribucion_valores()se calculan sobre los valores que se pudieron comparar, y los que no —bytes inválidos, sobre todo— se descartaban dejando elestadoencalculada. Medido sobre una columna de cuatro filas con tres inválidas, la tabla publicabazcon proporción 1.00 cuandozes el 25 % de la columna. Ahora ese caso se declaracalculada_parcial, y la documentación dice sobre qué base están calculadas las proporciones. UnNAno cuenta como descarte: es una ausencia declarada.reportar()ya no aborta, y el resultado decomparar_equivalencia()se puede imprimir. Los dos morían sobre un valor declaradobytes—nchar()no puede medir caracteres ni ancho en esa codificación—, el primero con «number of characters is not computable» y el segundo alprint(), después de comparar bien. Bastaba con que el valor fuera la moda de una columna, o con que un lado de la comparación llevara la marca aunque los bytes fueran idénticos. Los valores declarados se rinden ahora a la forma que muestra la consola, que es ASCII, y con eso los patrones publicados también dejan de interpretarlos.La clave de identidad es inyectiva. El escape de un valor declarado
bytesproducía un texto que el usuario puede escribir —los cuatro bytes deañoy el texto literal de diez caracteres que los describe daban la misma clave—, así quen_distintosvolvía a contradecir aunique()de R. Y, por el orden en que se aplicaba, la misma cadena daba dos claves según qué la acompañara en el vector. Las dos cosas están cerradas.Encoding() == "bytes"se respeta también al decidir identidad y al publicar. Esa marca declara que el contenido no se interprete como texto, y el paquete sólo la respetaba al imprimir: la clave de identidad, la cardinalidad y la evidencia de duplicados la ignoraban. Sobre la misma columna,unique()de R veía 3 valores distintos ylupapublicaba 2, mientras su propia consola los mostraba separados. Ahora la identidad los separa —yn_distintoscoincide conunique()—, la evidencia de duplicados conserva la declaración —se publicaa\xc3\xb1o, igual que en la consola— y el reporte HTML deja de interpretarla. Lo que no cambia, a propósito: el camino de análisis sigue marcando UTF-8 lo declaradobytescuando sus bytes son válidos, porque ahí —tolower(),trimws(), las expresiones regulares— interpretar no pierde nada. La regla es por destino: interpretar para analizar, no para publicar ni para decidir identidad.guardar_analisis(comprimir = "TRUE")se rechaza con el mensaje del paquete. La validación comparaba sobreas.character(), así que las cadenas"TRUE"y"FALSE"la pasaban ysaveRDS()las rechazaba después coninvalid 'compress' argument, un error crudo de R. Una validación existe para que ese error no llegue al usuario.-
La persistencia del paquete ya no depende del locale que la escribió. El formato RDS 3 —el que
saveRDS()usa por omisión— anota en la cabecera la codificación nativa de quien escribe, y al leer traduce desde ella el texto que no declara la suya. Escribiendo bajoLC_CTYPE=CanotaANSI_X3.4-1968; al releer, en Linux la conversión falla y los bytes se salvan por el camino del error, pero en Windows no falla —apaga el bit alto— yBásicovuelve comoBC!sico. Texto plausible, silenciosamente distinto:acumular_historico()llegaba a rechazar su propia corrida guardada.guardar_historico()yguardar_analisis()escriben ahora conversion = 2, que no anota codificación nativa y por lo tanto no traduce nada.Para un perfil guardado con
saveRDS()el remedio es esa misma línea —saveRDS(perfil, archivo, version = 2)—, y la documentación decomparar_perfiles()lo nombra. Lo que el paquete no hace es disimularlo: un perfil releído con el texto cambiado está alterado, y la comparación informa la diferencia en vez de igualarla. La identidad de un registro ya no depende de cómo R marcó su texto. La clave que decide si dos registros son el mismo trataba una cadena marcada
latin1como bytes inválidos y escapaba su contenido, así que el mismo texto daba dos claves según la marca —=B%C3%A1sicomarcado UTF-8 o sin marcar,=B%5C341sicomarcado latin1—, bajo cualquier locale. Importa fuera del laboratorio porque Windows marcalatin1donde Linux no:acumular_historico()podía rechazar, o duplicar, la misma corrida según la máquina que la guardó. La copia que se exhibe ya convertía lo que trae codificación declarada; la clave no. Era la misma pregunta contestada en dos lugares con dos criterios.Encoding() == "bytes"se respeta. Esa marca no es una codificación más: es la declaración de que el texto no se interprete. El paquete lo declaraba UTF-8 y publicaba el carácter; ahora publica lo que publica R.La publicación también es independiente del locale. Las propuestas de proponer_modelo() y las tablas que muestran los métodos print.* ya no fallan por codificación en ningún locale, incluido LC_CTYPE = C. La copia que se exhibe convierte lo que trae codificación declarada —latin1 o UTF-8—, declara como UTF-8 los bytes válidos sin declarar —R los escapa como <U+00F1> donde el locale no los represente— y deja los bytes restantes tal cual mientras print.data.frame() pueda con ellos. Bajo un locale UTF-8 la salida es la misma que la de R —medido en 39 tipos de columna por 3 formas de row.names por 3 anchos por 2 locales, 702 cruces— con una excepción que se sigue de lo anterior y conviene tener presente: si una clase suya define un format() que consulta Encoding(), va a ver la marca de la copia y no la del original, así que puede decidir distinto. Bajo un locale que no puede representar un carácter la salida difiere a propósito, y para mejor: donde R publica los bytes escapados en octal, lupa publica el punto de código, <U+00F1>, que dice qué carácter era. Sólo cuando R no puede imprimir —una columna de listas con bytes inválidos, donde aborta en cualquier locale— se reintenta escapando con el octal que el propio R usa, \377. Ese reintento transforma el marco entero, no sólo la celda que impedía imprimir: duplica las barras de todas las columnas, porque si no lo hiciera el byte escapado y el texto literal de la misma forma publicarían lo mismo. Es el precio de que la representación sea reversible, y sólo se paga en un marco que R no podía imprimir de ninguna manera. El octal no es un detalle: escapar como
hacía que el byte 0xff y el texto que un usuario puede escribir se publicaran iguales, y R los distingue. Eso no vuelve infalible a print(): si una clase del usuario tiene un format() que da error, ese error le llega sin alterar, que es lo que corresponde. Y un format() que consulte Encoding() verá las marcas de la copia, no las del objeto original. Además, la clave interna de comparar_perfiles() usa la misma representación por bytes que el resto del paquete y dos perfiles idénticos no emiten avisos espurios. Los métodos print y los avisos tampoco publican bytes crudos. Los avisos y encabezados que componen texto del usuario —nombres de columna, de tabla, de fuente— lo declaran antes de publicarlo. Bajo LC_CTYPE = C ya no aparecen frases a medias, con el texto del paquete legible y el nombre del usuario en bytes: donde el locale no puede representar un carácter, se escapa como <U+00F1> en toda la línea. Una prueba recorre diez canales —los métodos print y los avisos— por las dos corrientes de salida y falla si encuentra un byte crudo. Los mensajes de error quedan fuera de ese alcance y lo dicen: son 651 sitios y siguen publicando el nombre tal como el usuario lo entregó, que es también como lo publica cualquier paquete de R.
La configuración de una corrida se compara por bytes, como sus datos. La comparación de filas de configuración de acumular_historico() usaba una igualdad que consulta la codificación: dos filas con los mismos bytes y distinta marca —una leída de un archivo, otra recién calculada— se consideraban distintas bajo un locale no UTF-8 y abortaban la acumulación entera.
El efecto observado ya no depende de la estimación del plan. Una acción seleccionada que deja
n_cambiadas = 0se registra comofallidaaunquen_afectadasseaNA, cero o un número editado que no corresponde; el motivo distingue el efecto medido de la estimación ausente. Se agregó una prueba sobre un plan editado que también cubre acciones borradas y parámetros inválidos, y se auditó la capa completa: de las construcciones de ausencia encontradas, sólo la guarda de.motivo_efecto_accion()apagaba un control.La normalización vectorial declara los bytes UTF-8 válidos antes de aplicar los pasos optativos y transforma ligaduras por punto de código.
ligadurasyanchocumplen ahora igual bajoLC_CTYPE = "C"y bajo un locale UTF-8.La evidencia que publica decimales fijos usa la misma marca de
OutDecque las columnas:sprintf()ya no deja puntos mezclados con comas en un mismo perfil.El diagnóstico
casi_duplicados_vocabularioqueda documentado como una evaluación independiente por columna. No se introduce un caché transversal: la medición encontró tres pares, todos en columnas pequeñas o constantes, en las tablas reales disponibles, y cada columna conserva su propia cobertura y sus hallazgos.bloque_muestra = "solo_agregados"ahora se respeta también conuniverso = "muestra_motor": los agregados siguen usando la relación muestreada, pero no se leen filas ni se materializa un spool. El plan y la corrida comparten esa decisión, ymeta$materializacion/meta$bloquesdejan el estadono_solicitadoyfilas_vistas = 0cuando corresponde.Las lecturas DBI marcan como UTF-8 los bytes válidos antes de perfilar o guardar texto; las comparaciones de
NoNulo,FormatoyValoresPosiblesPorExtensionusan claves por bytes. La evidencia de duplicados aproxima nombres y valores sin depender deLC_CTYPE, mientras los nombres de columna publicados conservan sus bytes y su marca de entrada.Las secuencias
integer64registran los métodos debit64antes de medirse aunque el paquete no esté adjunto; sibit64no está instalado, sus cinco medidas quedan enNAycobertura_diagnosticosdeclara la no evaluación.La moda y las constantes numéricas que se publican en texto usan notación fija con la precisión completa, independiente de
scipeny dedigits, y conservanOutDec.La clasificación de posibles datos personales usa esa representación fija, así que
scipenydigitsya no cambian la clasificación ni borran demetalos centinelas numéricos declarados. Las claves de deriva e histórico comparan nombres y configuraciones por bytes, incluso después desaveRDS()y al cambiar de locale.Las sugerencias de ausencia estructural componen sus nombres con una copia marcada como UTF-8, sin cambiar los nombres publicados de la tabla; el código que se puede copiar y pegar queda igual bajo
LC_CTYPE = "C".Las publicaciones por
climarcan en un único punto los textos UTF-8 válidos que vienen de nombres, valores y motivos del usuario. BajoLC_CTYPE = "C"ya no se publican sus bytes crudos; los textos inválidos siguen su camino sin ser marcados.El histórico conserva el mismo desempate por fecha e identificador y la misma huella de configuración al cruzar locales; acumular una corrida persistida es idempotente y detectar deriva ya no emite avisos por sus claves internas.
Texto declarado independiente del locale
-
La resolución de factores y las agrupaciones por valores de texto ya no dependen de la marca de codificación.
.resolver_factor(), las rutas defactor()/split()y la detección de variantes usan la misma representación de trabajo que los identificadores; las etiquetas que se publican conservan el texto original. -
Se auditaron los 39
switch()del paquete. Los que reciben una declaración usan vocabularios cerrados y no cambian entre marcas; el que conmutaba sobre el par dimensión-factor queda cubierto por la clave canónica. También se revisaronmatch.arg(),merge()por texto,tapply()e indexaciones nominales; las indexaciones que podían recibir nombres declarados resuelven por posición o por la misma clave canónica.
Identificadores declarados independientes del locale
- Las dimensiones, factores y demás identificadores declarados ya no se comparan por su marca de codificación. Las validaciones de marcos, modelos, granularidades, fronteras, métricas, reglas, pesos, colecciones y configuraciones usan claves estables derivadas de los bytes; las salidas conservan el texto original y las secuencias de bytes distintas siguen siendo distintas.
-
Los ejemplos con texto no ASCII usan escapes
\uXXXXdentro del código R. Esto permite queR CMD checkre-encode los ejemplos bajoLC_ALL=Csin cambiar el texto que muestranmarco_calidad(),modelo_calidad()nimetricas_referencial().
Escritura independiente del locale
-
reportar()ya no convierte el HTML al locale de la sesión. El documento se serializa como bytes UTF-8, se verifica antes de copiarlo al destino y se comprueba que el archivo final coincida completo. Cualquier fallo de escritura, copia o verificación ahora aborta con un error propio en vez de devolver un HTML parcial como si hubiera terminado bien.
Lo que se mide contra lo que se declara
-
Los nombres de columna publicados conservan bytes y marca de codificación.
.marcar_utf8_tabla()ya no reetiquetanames(): los nombres son datos del usuario y.nombres_para_operar()alcanza para las comparaciones y ordenamientos internos. Así,perfil$columnas$columnay los nombres que publican claves, relaciones, dependencias, hallazgos, patrones, remediación y reportes siguen indexando la tabla de entrada bajo cualquierLC_CTYPE. -
El manejo de nombres de columna dependía de
LC_CTYPEen un nivel más profundo.perfilar(), las claves, los consumidores del perfil y las validaciones de declaraciones usan ahora una única representación de trabajo derivada de los bytes (.nombres_para_operar()), junto con.nombres_make_names()y.nombres_unicos()deterministas. Sólo se normaliza para comparar o desambiguar: las salidas siguen publicando el nombre original y dos secuencias de bytes distintas siguen siendo columnas distintas. Esto cubre también dependencias, deriva, Benford, normalización, relaciones, duplicados aproximados, remediación y las rutas DBI. -
Un nombre de columna con bytes latin1 sin codificación declarada abortaba
perfilar(), y abortaba en dos sitios distintos según el locale. Es el caso de un CSV en español leído conread.csv(check.names = FALSE)o confread(). Las operaciones internas que exigen texto válido usan ahora una forma determinista derivada de los bytes, sin adivinar la codificación; el nombre que se publica conserva sus bytes originales y el hallazgo declara que la codificación no se pudo establecer. Los valores ya tenían ese camino declarado; los nombres no tenían ninguno. Se cubrieron además los consumidores de nombres en claves, relaciones, dependencias, normalización, deriva, remediación y las puertas DBI, y no sólo los dos que aparecían en la traza. -
Una columna
rawrompíadistribucion_valores()yanalizar()dentro derbind, con un error de R que se filtraba al usuario, mientrasperfilar()la aceptaba..texto_analizable()promete texto y ahora lo cumple también pararaw(01,02,03), conservando el vector original para la identidad. -
El marcado de la entrada decidía con
identical(), cuya respuesta depende del locale. Con los mismos bytes, una cadenaunknowny otraUTF-8son idénticas bajo un locale UTF-8 y distintas bajoC, así que el marcado de nombres y de niveles de factor acertaba en un locale y era un no-op en el otro. Ahora decide comparando la marca, que es lo que el marcado cambia. -
Una tabla sin
names()moría dentro del marcado, antes de llegar a ninguna validación. Lo que no es texto pasa intacto. -
perfilar()podía abortar bajoLC_CTYPE=Cpor un nombre de columna con UTF-8 válido marcado comounknown. El marcado de entrada ahora cubre tambiénnames(datos), no sólo los valores. -
Encoding() == "bytes"no distinguía bytes arbitrarios de UTF-8 válido. Las dos APIs públicas,perfilar()einferir_tipo(), marcan como UTF-8 los bytes válidos antes de analizarlos; los bytes inválidos siguen declarados como texto no descifrable. -
La identidad de una columna podía ignorar valores con bytes UTF-8 inválidos.
n_distintos,moda,frecuencia_moday sus denominadores ahora comparan la representación almacenada mediante claves de bytes, sin usar esa clave para plegar ni reinterpretar el texto. -
Una columna compuesta sólo por valores de texto inválidos publicaba ceros de reparación y
unicode_evaluado = TRUE. Esas pasadas ahora quedan enNAy la bandera declara que no se evaluaron;n_codificacion_invaliday su hallazgo de error conservan la advertencia sobre los valores excluidos. -
perfilar()abortaba con un CSV en español bajo un locale que no fuera UTF-8.read.csv()deja los textos conEncoding()enunknown—el caso más común que existe en español— y cualquier operación que exija UTF-8 los rechaza: se encontraron dos caminos que morían así. La codificación se declara una vez, al entrar, y sólo sobre lo que ya es UTF-8 válido; lo que no lo es sigue tratándose como texto no descifrable, que es lo que corresponde. -
n_numeros_textopublicaba0cuando sí había números escritos como texto. Con tres de cuatro valores numéricos-como-texto, el campo decía «ninguno» al lado deproporcion_numeros_texto = NA: dos campos describiendo el mismo hecho y diciendo cosas distintas. Ahora se distingue no haber visto ninguno —0, que es un hecho— de haberlos visto y no haberlos contado por no alcanzar el umbral —NA—. -
Un valor de la lista de centinelas que aparece una sola vez, en su lugar, se acusaba de ser una ausencia codificada. En un catálogo de dos tramos —
1:1000más2001:3000— el999que trae la propia numeración salía señalado. El paquete ya declaraba el criterio correcto —un centinela cumple «las tres cosas a la vez: que sea un valor extremo, que se repita, y que tenga forma de centinela»— y la lista por omisión se aplicaba sin la del medio. Un candidato que aparece una vez y cae dentro del rango de los demás valores ya no entra; uno que aparece una vez fuera del rango sigue entrando, porque ahí la rareza está en el valor y no en su frecuencia. La regla afina la lista de por omisión, que es la que el paquete aplica sin que nadie la pida; una lista que el usuario eligió —sentinelas_naniar— no se angosta, porque pedirla es optar por una heurística más ancha. -
Con el presupuesto de memoria agotado,
detectar_relaciones()devolvía una tabla vacía. Quien la imprimía veía encabezados y nada, y una tabla vacía se lee como «no hay relaciones entre estas tablas» cuando lo que pasó fue «no se comparó ninguna»: los pares quedaban sólo en un atributo. Ahora cada par sale declarado concardinalidad = "sin_comparar"y su motivo, igual que en las otras podas de la misma función. -
Bajo
LC_CTYPE=C, el paquete emitía avisos de defecto propio sobre datos ordinarios. El pliegue a minúsculas de los hallazgos usabatolower(), que en ese locale no baja los acentos:CAFÉycafédejaban de agruparse juntos, la trazabilidad sí los juntaba, y el paquete informaba —correctamente— que había una inconsistencia suya. Los cuatro sitios usan ahora el mismo ayudante independiente del locale que ya existía en el paquete, que además cubre laIturca. -
La deriva decidía el veredicto con la resta en coma flotante.
0,70 - 0,65da0,049999999999999933y0,75 - 0,70da0,050000000000000044, así que dos pares que publicaban el mismodelta = 0.05recibían «estable» y «mejora». Ahora el umbral se compara con la misma tolerancia que el paquete ya usaba para los pesos. -
La descripción de una deriva contradecía a su propio dato. Afirmaba «cambió el resultado» sobre filas con
delta = NA—el resultado anterior no se evaluó—, y su guarda paradelta == 0no corría nunca: usabaisTRUE()sobre un vector con un elemento por par, que con más de un par devuelveFALSEincluso con todos los deltas en cero. ElNAahora dice que no se puede comparar, y el cero dice que se mantuvo. -
evaluar()rechazaba una medición válida con un diagnóstico falso. Una medición que sí viene demedir()y quedó sin filas —ninguna métrica aplicable, cosa quemedir()declara en sucobertura_metricas— era rechazada con «debe ser un data frame no vacío producido pormedir()». Las tres puertas —evaluar(),tablero_calidad()eindice_calidad()— ahora separan los dos casos y propagan el motivo quemedir()ya había declarado. -
Dos puertas más publicaban valores que
perfilar()enmascara. Laevidenciadedetectar_discordancias()citaba las filas discordantes con sus valores crudos —documentos, cuando la columna lo es—, y losejemplosdedescubrir_patrones()llamada suelta salían enteros. No fallaba la capa de protección: esas dos funciones nunca habían pasado por ella. Ahoradetectar_discordancias()clasifica las columnas de la señal con la misma herramienta que el resto del paquete y enmascara sólo las personales —el número de fila se conserva, que es para lo que existe—, ydescubrir_patrones()enmascara sus ejemplos cuando la forma alcanza para clasificarlos, como en un correo. Un número de ocho dígitos sin nombre de columna se sigue publicando: su forma sola no alcanza para afirmar que es un documento, y el paquete no conjetura por debajo de lo declarado. -
secuencia_entera_densagobernaba cuatro escudos y se había estrechado pensando en uno. Volvió a medir sólo la cobertura de una numeración, que es lo que su nombre dice, y los escudos de forma —patron_raro,tipo_declarado_distintoy el diagnóstico de identificador— vuelven a colgar de ella. Antes, cuatro copias de un valor legítimo del propio rango bastaban para que el paquete acusara a999de ser una ausencia codificada apareciendo una sola vez, en su lugar, dentro de1:1000. - La guarda de centinelas se decide por candidato, no por columna. Un valor entra si cae fuera del rango de la numeración o si su frecuencia sobresale; esas dos señales no son la frecuencia sola, y por eso resisten el caso en el que cinco valores legítimos comparten la frecuencia del centinela y aplanan cualquier comparación entre frecuencias. Decidirlo por columna hacía que un candidato con evidencia arrastrara a otro sin ella.
-
La misma declaración escrita de otra forma daba el resultado contrario.
sentinelas_numericosse comparaba conidentical()contra la lista por omisión, que compara la representación y no el conjunto: la lista tal cual detectaba 0 y esos mismos cinco valores al revés, como enteros o con un duplicado detectaban 201. Ahora se normalizan como conjunto en las dos puertas que reciben el argumento,perfilar()yperfilar_dbi(). -
relaciones_coleccion()publicaba el mínimo y el máximo de una columna de documentos. El campodetallede una poda por rangos disjuntos explicaba la poda escribiendo los cuatro extremos crudos —"[45120001, 45120040] y ..."—, mientrasperfilar()yperfilar_dbi()enmascaran esos mismos estadísticos sobre esa misma columna. El mínimo de una columna de documentos es el documento de una persona real, y el par acota a los demás. Ahora un rango cuyo extremo llegue al piso de la protección sale como[valor protegido]; los rangos que no identifican se siguen publicando, que es lo que hace útil la explicación. -
Un valor subnormal se publicaba como un dato calculado. El caso real: escribir una columna de enteros grandes en una base la guardó como doble con los bits mal interpretados, y el perfil publicaba
moda = 1,06e-314con estado sano, porque el dato ya venía así del origen y ninguna comprobación cruzada podía contradecirlo. El hallazgovalores_subnormaleslos cuenta, da sus filas y los marca comosospechoso. No es concluyente y no lo afirma: un cálculo que desborda por defecto cae legítimamente en ese rango —2^-1050da 8,3e-317—. -
agregar(funcion = "ratio")contaba como falso lo que no era 0 ni 1. Con una medida declarada booleana y un valor intermedio devolvía la proporción de unos sin decir nada, mientraspromediosobre los mismos valores devolvía la media. Se comprobaba el tipo declarado, no los valores. -
comparar_perfiles()abortaba contra un perfil de otra versión. Un perfil guardado hace meses puede no traer un campo que esta versión compara: en cuatro de ellos abortaba con el mensaje interno de R, y en dos publicaba una fila de cambio atribuida a los datos cuando lo que faltaba era el campo. Ahora compara la intersección y declara lo ausente diciendo de qué lado falta. -
sugerir_clave()ofrecía como clave lo quedetectar_claves()excluye. Un importe con decimales puede identificar cada fila y no ser una clave. No se oculta que identifica —es un hecho medido—: se dice la reserva en el motivo y la columna queda al final del orden. -
Se proponía una escala sobre una columna sin un solo dato observado. Recibía
continuacon confianza 0,65, la misma cifra que sobre mil observaciones. Ahora dicedesconocida. Donde la clase determina la escala —logicales binaria,Datees temporal— la propuesta se mantiene. -
medir()no tenía por dónde recibir qué proteger. Aceptabaproteger_datos_personalespero nocolumnas_personalesnivalidadores_personales, así que una columna que sólo es personal porque el usuario lo dice quedaba sin proteger en el camino de métricas. - Un parámetro vacío en el plan fallaba con un mensaje interno de R. Quien edita un plan no conoce las funciones internas, y ése es el caso de uso que la capa declara. Ahora el motivo nombra el parámetro y la acción.
-
max_largo_valor = Inf, documentado como «sin tope», abortaba el camino LSH con valores de 9.999 bytes o más. -
La normalización era cuadrática sobre un texto largo.
regmatches()indexa por carácter, y en UTF-8 eso obliga a recorrer la cadena desde el principio en cada coincidencia. Sobre 160 KB: de 41,1 s a 0,079 s. -
comparar_perfiles()no podía declarar que había cambiado la vara. Seis varas que deciden si un hallazgo se emite y con qué severidad no viajaban enmeta. Subirumbral_faltantes_errorde 0,4 a 0,9 sobre la misma tabla publicabaseveridad_hallazgo / atenuadoy nada más: el efecto sin la causa. Ahora una filaconfiguracion_umbralesla nombra, y con el dato cambiado y la vara quieta esa fila no aparece. -
Un cambio de almacenamiento se leía como deriva de los datos. La misma columna guardada como número y como texto publicaba seis diferencias materiales —longitudes, variantes unicode y números escritos como texto— y ninguna decía que lo que había cambiado era cómo se guarda. Los guardas de tipo consultaban el tipo inferido, que en ese par da
doblede los dos lados y borra justo la diferencia que había que ver. Ahora se lee el tipo declarado y la columna queda encolumnas_no_comparablescon el motivotipo_cambiado:texto_vs_no_texto.factorcuenta como almacenamiento de caracteres, y ante un vocabulario ajeno al de memoria —un tipo SQL— no se afirma nada. Entre dos textos, una diferencia de longitud sigue siendo deriva. -
Perder la zona horaria se leía como un dato faltante. Una columna
POSIXctguardada como texto pierde la zona, y el conteo de filas cuya fecha civil difiere de UTC salía como diferencia material contra un lado que no tiene zona que medir. Ahora se declara contipo_cambiado:con_zona_vs_sin_zona. Los extremos de fecha siguen comparándose: al perder la zona los instantes que la columna denota son realmente otros —tres horas de corrimiento enAmerica/Montevideo—, y eso es lo más importante que hay para informar. Entre dos columnas que sí llevan zona, el conteo se compara como siempre. -
Se publicaba una moda donde no había ninguna. Cuando todos los valores válidos aparecen una sola vez no hay moda: lo que se publicaba era el ganador de un desempate, y el desempate sigue el orden de ordenamiento, que depende de cómo esté guardada la columna. La misma
c(-5.5, -1, 0, 3.75)daba-5.5como número y-1como texto. Ahoramodaqueda enNAyfrecuencia_modase conserva, que es la evidencia de por qué. Con un solo valor distinto sí hay moda. Sobre el banco real son 4 columnas de 38, las cuatro identificadores únicos, y ningún hallazgo cambia: 95 antes y 95 después, los mismos tipos en el mismo orden. Las tres puertas —memoria,resumen_tablay la muestra— dicen lo mismo, que era el riesgo real de arreglar una sola. La claseperfil_dbise asignaba en cuatro lugares y ahora sale de uno solo,.sellar_perfil_dbi(), para que ningún camino se saltee la regla. -
Un centinela escondido dentro de una numeración se callaba. La guarda de centinelas se apaga sobre una «secuencia entera densa», y la densidad contaba la cobertura del rango ignorando cuántas veces aparece cada valor: con
c(1:1000, rep(999, 100), 2001:3000), el grupo de las 1.100 primeras filas daba densidad 1 —mil valores distintos sobre mil posiciones— y callaba el999repetido 101 veces que la tabla completa sí acusaba. Ahora «numeración limpia» exige además que ningún valor sobresalga, con el mismo criterio que usavalor_concentrado. Lo que separa un centinela de una clave foránea no es el tamaño de la moda sino su forma: sobre las diez columnas numéricas del banco real el mayor cociente es 1,71 y el caso a atrapar da 101. Ningún hallazgo del banco cambia, y perder la condición de numeración no acusa por sí solo: sólo devuelve la columna a la mirada de la lista de centinelas. La comparación es contra la frecuencia típica —la mediana—, no contra el segundo valor: comparar contra el segundo supone que el centinela es la moda, y una refutación externa lo derrotó con un señuelo —1:60con cinco-9y un7repetido seis veces deja el cociente en 1,2 y callaba los cinco centinelas—. Y la condición sólo alcanza a columnas con forma de numeración, donde los valores no se repiten: una clave foránea repite todo, no es una numeración y conserva su condición. Se mide por dos vías, y cada una cubre lo que la otra deja pasar: contra el segundo valor más frecuente —que ve un centinela masivo dentro de una clave foránea, donderep(1:40, each = 4)con cien-9callaba las cien— y contra la frecuencia típica cuando ésta vale 1 —que ve el centinela cuando un señuelo legítimo infla el segundo puesto—. Sobre el banco real ninguna de las diez columnas numéricas sobresale y los hallazgos siguen en 92. Y ni siquiera dos vías alcanzaban: varios centinelas con la misma frecuencia se cubren entre sí —{499, 499, 499, 2, …}deja «máximo igual al segundo» y callaba mil cuatrocientos noventa y siete valores—. Ahora se mide el salto entre frecuencias consecutivas, mirando sólo las primeras posiciones: 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. Sobre el banco real ninguna columna pasa de 1,71 y los casos a atrapar van de 4,75 a 249. -
Un grupo bajo el umbral de densidad acusaba lo que la tabla completa callaba.
perfilar_por()reubica a la cobertura los hallazgos de centinela de una columna que es densa en la tabla entera, pero sólo cubríafaltantes_disfrazados: un grupo emitíafaltantes—«0 ausentes reales y 4 disfrazados»— y ese tipo se escapaba. Ahora entra también, sólo cuando el grupo no tiene ninguna ausencia real: un faltante de verdad no es de la partición y se sigue informando. -
La protección de datos personales pasa a ser por columna. Era por valor y global: si
cedulaestaba protegida y contenía"S/D", ese texto se enmascaraba en todo el perfil, incluida la columnasexo—que no tiene un solo dato personal y donde"S/D"significa «sin dato»—. El lector no podía distinguir una columna que trae datos personales de una que sólo comparte vocabulario. Ahora se reemplaza lo que describe a la columna protegida: su moda, sus estadísticos, sus ejemplos y la evidencia de sus hallazgos. Lo que no tiene columna a la que atribuirse sigue enmascarado en toda la salida —un hallazgo de filas duplicadas muestra filas enteras, ygeneral, la cobertura y los formatos de fecha describen la tabla, no una columna—. Con un piso: un valor de una columna protegida que identifique —seis caracteres o más, el mismo corte con el que la batería de fugas decide qué cuenta como filtración— no se publica en ninguna parte, y eso alcanza también a los campos numéricos. Sin el piso, una columna que el clasificador no marcó publicaba los documentos de la protegida con sólo repetirlos: medido, una copia llamadacodigo_operaciony hasta un texto libre con el documento adentro publicaban tres de cuatro documentos de ocho dígitos, y los estadísticos de orden de una copia clasificada con poder discriminantedebilpublicaban dos documentos exactos sobre doce filas. El vocabulario corto no entra en el piso:"S/D"sigue publicándose ensexo. El piso alcanza además la forma sin separadores —"771.771-01"es el mismo documento que"77177101"— y lo aplican tambiénanalizar()ydistribucion_valores(), que protegían sólo por columna: sobre la misma tabla,perfilar()tapaba el documento adentro de un texto libre yanalizar()lo publicaba envariables$niveles_observados. Sobre el banco real no enmascara ni una celda de más: 25 antes y 25 después, con los mismos 92 hallazgos. -
Comparar un perfil protegido contra uno sin proteger no declaraba la asimetría. La salida publicaba el rango que el lado protegido oculta y la diferencia se leía como deriva del dato, cuando lo que cambió es la política. Ahora se declara con su propia fila,
configuracion_proteccion, severidaderror, en las dos direcciones —la misma forma que ya teníanconfiguracion_umbralesyconfiguracion_normalizacion—. No era una fuga: quien corre esa comparación ya tiene el perfil sin proteger. -
Una columna llamada
sepabortaba la corrida. Los nombres de columna son datos del usuario y llegaban como argumentos con nombre apaste, así que una columna llamadasep,collapseorecycle0chocaba con sus formales y reventabadetectar_duplicados_aproximados(),detectar_claves()yperfilar(), con un mensaje que no nombraba ni la columna ni la causa. El patrón estaba en cinco lugares y uno solo tenía la protección que lo evita. -
detectar_duplicados_aproximados()yperfilar_dbi()aplican el piso de la protección. El primero publicaba el documento en la misma cadena donde la columna protegida iba enmascarada. El segundo publicaba enresumen_tablalos estadísticos de orden de una copia numérica queperfilar()sí tapaba: por esa puerta el piso por valor es imposible —el resumen se calcula con SQL sobre la tabla entera y no se ven todos los valores—, así que se tapan los estadísticos de toda columna que comparte tipo personal con una protegida. No es una conjetura sobre los valores: es lo que la clasificación ya afirmó de las dos. -
Cincuenta y cinco comprobaciones no medían lo que decían. Corriendo cada archivo de prueba en su propio proceso, 17 de 192 fallaban mientras la suite entera daba
FAIL 0. Una sola causa:cliemite su salida como condiciones de mensaje, y dentro detestthatquedan atrapadas antes de llegar a ningún flujo —es lo que le permite sostenerexpect_message()—, así que una prueba que usabacapture.output(..., type = "message")osink()recibía un texto vacío y afirmaba que la salida no contenía lo que sí contenía. Ahora las recogesalida_cli(), que además quita los códigos de color y normaliza el espacio:cliajusta al ancho e inserta saltos dentro de una frase, y una búsqueda literal fallaba por dónde se cortó la línea y no por lo que decía.
Una declaración vale en todas las salidas, no sólo en la primera
-
El informe no publicaba las coberturas del objeto. Una medición donde una tabla entera tenía cero filas producía un informe idéntico al de una donde todo se midió:
reportar()no mostrabacobertura_metricas—qué métrica no se pudo medir y por qué— nicobertura_coleccion—sobre cuántas de las tablas declaradas se calculó el número—. El informe es la salida que más lejos llega, porque es la que se comparte. Ahora las publica, en la medición y en la evaluación, y no inventa la sección cuando no hay nada que declarar. -
analizar()publicaba el código deparseado como nombre de la tabla. Con una expresión larga,deparse()devuelve varias líneas: el nombre salía con tres elementos y con[valor protegido]incrustado, porque la capa de protección enmascaraba los números del propio nombre.perfilar()tenía el saneador desde antes, pero vivía dentro de su propio cuerpo —era local— yanalizar()no podía usarlo. Ahora vive en un solo lugar y las dos vías dan"datos"cuando la expresión no sirve como nombre. -
analizar()rechazaba una matriz queperfilar()documenta y acepta. La puerta de entrada exigía heredar dedata.frameantes de delegar en su propio motor. Ahora la matriz llega aperfilar()sin convertir, para que sea él quien la convierta y lo declare enmeta$entrada_convertida: convertirla en la puerta dejaba esa declaración enNAy el perfil aparentaba haber recibido una tabla. -
Documentadas las dos diferencias deliberadas de
analizar()respecto de llamar a las funciones sueltas: la conversión de matrices y la columnaproteccion_temporalque ganatemporal$resumencuando una columna temporal está protegida. -
La cobertura moría en el consumidor siguiente, en dos saltos distintos.
medir()declara encobertura_metricasqué métricas no se pudieron medir y por qué —«la entidad dependientebtiene cero filas»—, yagregar()lo descartaba en el primer salto: de ahí en más esa tabla era invisible y el conjunto valía lo mismo que si nunca hubiera existido. Yagregar()a nivel colección adjuntacobertura_coleccion—tablas declaradas, tablas en el número, tablas sin medir y una advertencia—, que nitablero_calidad()niindice_calidad()conservaban: una colección de dos tablas con una vacía publicaba0,667—que cubre una de las dos— sin nada que lo dijera. Las dos coberturas viajan ahora hasta el último consumidor, y el índice imprime la de la colección: un índice es un solo número, y una cobertura que vive sólo en un atributo no la lee nadie. -
Tres afirmaciones de la documentación que el paquete no cumplía. Una viñeta decía que en una columna protegida «las medias y los desvíos se mantienen»: la media se suprime —está en la lista y se rastrea como momento, distinto de los estadísticos de orden—, y el desvío se mantiene. Dos viñetas y los dos README decían «109 campos analíticos» cuando son 111. Y la tabla de evidencia de los README, encabezada por «entre las salidas de esa corrida», citaba un
tipo_hallazgoque esa corrida no produce y daba como ejemplo una evidencia que sólo aparece con la protección de datos personales desactivada. Las tres corregidas, y dos guardas nuevas las miden corriendo: el ancho del perfil contra el número publicado, y cadatipo_hallazgocitado contra los que esa corrida produce. -
perfilar_por()perdía las filas del grupo en blanco.x[[""]]devuelveNULLaunque el elemento exista —R no resuelve la cadena vacía como nombre—, y el recorrido de los grupos era por nombre: el grupo de los blancos recibíaNULL, quedaba con cero filas y se publicaba como «El grupo tiene 0 filas». Sobre 200 filas con 30 en blanco, la suma den_filas_grupodaba 170 y esas 30 filas no aparecían ni perfiladas ni declaradas. Se recorre por posición. -
La moda de una columna
integer64dependía de lo que el usuario tuviera cargado.bit64no registra sus métodos S3 cuando sólo está cargado, así que dentro del espacio de nombres del paquete cualquier operación de base sobre uninteger64—inclusox[1]— lo degrada adoublereinterpretando sus bits: la moda publicada era4.4501477170144e-308sinbit64adjunto y el valor correcto con él. La moda tiene ahora una rama propia que trabaja con la representación exacta y desempata por el orden numérico, igual que una columnadoubleequivalente. -
La proporción de compatibilidad de datos personales no decía su denominador. Una columna con ocho documentos y treinta y dos blancos publicaba
proporcion_compatible = 1—«100 % compatible»— calculado sobre ocho de cuarenta valores. El denominador no cambia —incluir los blancos volvería incompatible a toda columna medio vacía—, pero ahora se declara envalores_evaluadosyvalores_totales. -
Los hallazgos del resumen cuantitativo publicaban el denominador entero.
outliers,ceros_no_permitidosynegativos_no_permitidosse calculan sobre los valores que llegaron a número, pero declarabann_evaluadosigual al total de la columna: mil filas con cien que no convierten publicabann_evaluados = 1000sobre 900 valores evaluados. El alcance ya estaba declarado enn_valores_excluidos_resumeny encobertura_diagnosticos, pero quien lee una fila dehallazgosno está obligado a cruzarla con otra tabla. El paquete ya hacía este mismo ajuste cuando hay un universo declarado poraplicabilidad; faltaba el segundo caso. Los diagnósticos que cuentan filas —filas_duplicadas— conservan la columna entera, que es su denominador. -
Comparar dos resúmenes con alcances distintos se atribuía a los datos. La misma columna, una vez numérica y otra como texto con un valor que no convierte, producía «Cambió el rango observado de la columna» —
[10, 100]contra[10, 90]— cuando el valor seguía ahí y lo que cambió fue que quedó fuera del resumen. La deriva declara ahora una filaalcance_resumen, con el mismo mecanismo que ya usa para las políticas de centinelas y de aplicabilidad. -
Un valor en blanco quedaba fuera del resumen sin declararse.
trimws()deja" "en""y elnzchar()que gobernaba la cuenta lo descartaba, así que una columna con 900 valores"10"y 100 en blanco publicaba la media sobre 900 filas conn_valores_excluidos_resumen = 0y estadocalculados. Los mismos 100 valores como texto ilegible sí se declaraban. Un blanco no es una ausencia declarada —el paquete dejan_faltantesen cero y lo cuenta enn_blancos—, así que su exclusión se declara como cualquier otra;NAsigue informándose como faltante, que es su lugar. Vale para la conversión a número y para las dos rutas de fechas. -
El estado del resumen de fechas nombra la razón correcta.
calculados_sobre_diasdecía que habían quedado afuera períodos de mes, y se usaba también cuando lo que quedaba afuera eran valores ilegibles o en blanco. Ahora ese caso dicecalculados_sobre_valores, el mismo vocabulario que la conversión a número. -
Retirar la clave declarada se leía como un hallazgo resuelto. La misma tabla perfilada con
clave = "id"y después sinclaveinformabaclave_no_unicacomoresueltocon severidadok: la clave seguía duplicada, y lo único que había cambiado era el argumento. La documentación declara lo contrario —«dejar de mirar no es lo mismo que arreglar»—. La causa de fondo era que el perfil no registraba sus propias declaraciones:meta$claveguarda el resultado de comprobarla y queda vacío cuando sale limpia. El perfil ahora registrameta$declaracion_claveymeta$declaracion_aplicabilidad, y la comparación distingue las dos cosas. -
Cambiar
aplicabilidadentre dos corridas se atribuía a los datos. Esa regla redefine el universo aplicable, así que la deriva emitía filas de cambio sobre faltantes, cardinalidad y rango cuando lo único que cambió fue el argumento. La comparación ya declaraba la comparabilidad para la política de patrones y para la de centinelas; ésta era la tercera y faltaba. -
La reparación de codificación era cuadrática en el largo del texto.
.ftfy_restaurar_a0()acumulaba byte por byte conc()y copiaba la cola entera en cada vuelta. Medido: 20 KB tardaban 0,29 s y 160 KB, 11,22 s —cuadruplicar el largo multiplicaba por 37 el tiempo—, y perfilar una tabla de tres filas con un valor roto de 160 KB tardaba 65 s. Ahora es lineal: los mismos 160 KB, 0,40 s, con salida idéntica en toda la batería de casos. -
La reparación cambiaba el texto declarando que no lo cambiaba. Con
LC_CTYPE = C,enc2utf8()reemplaza un byte que no forma UTF-8 válido por su escape literal, así quecaf<0xe9>salía como los siete caracteres ASCIIcaf<e9>conestado = "no_parece_roto"—y en otra configuración regional el byte se conservaba—. Si no se reparó, ahora no se cambia. -
Los perfiles de madurez de fábrica premiaban el defecto.
perfiles_madurez()genera reglasResultado > umbralsin consultar la orientación de la medida, así que sobre una métrica orientada adefecto—donde más alto es peor— la regla quedaba invertida: una tabla 100 % duplicada daEntidadDuplicada = 1y se daba por cumplida en los tres perfiles, mientras la tabla limpia —0— no cumplía ninguno. El mecanismo para consultarla ya existía:regla_evaluacion()documenta que la condición puede declarar un segundo argumentoorientacion. Ahora la fábrica lo usa, e invierte con1 - valor, la misma convención quetablero_calidad()aplica a sus componentes de defecto. Las métricasconformidadno cambian. -
Un perfil de madurez ya no juzga una métrica no acotada. Una métrica que declara
orientacion = "no_aplica"es, por definición del paquete, no acotada; un umbral en[0, 1]no puede juzgarla, y antesResultado > 0.5devolvía «cumple» para 30, 60 y 90 días de atraso por igual. Ahora la evaluación se detiene nombrando la métrica y el motivo, para que quien evalúa declare una regla con la escala que le corresponde. -
Guardar una columna como
integer64cambiaba lo que el paquete publicaba sobre ella. Tres desvíos con la misma raíz, encontrados comparando la misma columna descrita por dos caminos:-
bit64enmascaraorder()cuando está adjunto, y dentro del espacio de nombres de un paquete no lo está: ahíorder()es el de base, que sobre uninteger64ordena por los bits deldoublesubyacente y manda los negativos al final. Con eso, ante un empate la moda de una columnainteger64salía1donde la misma columna endoubledaba-999—y las tres puertas del perfil DBI heredaban la diferencia—. Ahora el orden pasa por un ayudante que usabit64::rank.integer64(), correcto también por encima de 2^53. - La detección de numeración densa aceptaba
enteroydoblepero nointeger64, que es la tercera forma de guardar lo mismo. Sin numeración detectada, el999de una columna1:1000dejaba de estar protegido por «esto es un identificador» y el mismo dato publicabafaltantes_disfrazadosdonde enenteropublicabaposible_identificador. Por encima de la precisión dedoubleno se mide y se declara, en vez de inventar una densidad sobre números redondeados. - La ley de Benford excluye
integer64por tipo desde el primer día, pero no lo declaraba: la misma columna producía una fila de cobertura enenteroy silencio eninteger64. Un diagnóstico que no corre se declara, también cuando no corre por el tipo.
-
-
n_cambiadascontaba valores presentes y no cambios, en las dos conversiones. Catorce acciones de la capa de remediación cuentan los cambios reales;convertir_tipoyconvertir_fecha_confirmadacontabansum(!is.na(x)). Se ve cuando la conversión es una identidad: planificar sobre una columna de texto y aplicar sobre la misma tabla ya convertida a entero dejaba la columna bit a bit igual y el registro decíaejecutadaconn_cambiadas = 4. Ahora cuentan los cambios, y con eso la acción sin efecto se registrafallidacon su motivo —que es lo que la documentación prometía— sin tocar esa lógica. -
Los duplicados se agrupaban mal en columnas
integer64. El método debit64paraduplicated()ignorafromLast, así que de un grupo de dos filas idénticas se marcaba una sola: el plan declaraba 2 filas, la acción marcaba 1, la otra quedaba sin.grupo_duplicado—aunque participa— y el hallazgo sobrevivía al re-perfilar. La misma tabla endoublemarcaba las dos. Ahora las tres cosas —qué filas se repiten, cuáles participan y en qué grupo— salen de los mismos códigos, y no dependen del tipo de la columna. -
El número del plan y su unidad de conteo podían hablar de cosas distintas.
unidad_conteose heredaba del hallazgo pisando lo que la acción declaraba, así queconvertir_tipo—que cuenta valores presentes— quedaba conunidad_conteo = "columna": el plan decía «5 columna» sobre una tabla de una sola columna, yguiar_limpieza()lo repetía en pantalla. La herencia ahora sólo se aplica cuando la acción no declaró su unidad. -
La configuración regional decidía qué patrones y qué etiquetas se publicaban.
table()ordena sus niveles con la intercalación del locale ysort()conserva ese orden en los empates, así que el desempate quedaba en manos de la máquina. Conmax_patrones = 2y un empate por el segundo puesto, la misma tabla publicabaA+ a+en una sesión yA+ Aaen otra —no es orden de filas: son patrones distintos, y con ellos cambian los ejemplos—; y con ocho etiquetas de faltante disfrazado de igual frecuencia y seis lugares en la evidencia, una sesión listabaSIN DATOy la otra no. Ahora las dos desempatan por bytes. El paquete ya había arreglado esta familia en el recorte de pares de duplicados y ya usabamethod = "radix"para la moda; el arreglo no había llegado a estos dos sitios. -
analizar_tiempo()hacía desaparecer las columnas que mezclan días y meses. Bastaba un formato de mes confirmado para abandonar la columna entera: no salía fila de resumen, no salía encolumnas_omitidas, ycolumnas_analizadasseguía nombrándola —un objeto que se contradice a sí mismo—.perfilar()sobre esa misma columna la resume bien, y la documentación declara ese trato para las columnas mixtas: las dos salidas del paquete decían cosas incompatibles. Ahora se resume sobre las fechas completas, conn_fechas_excluidas_granularidadyestado_resumen = "calculados_sobre_dias", el mismo vocabulario que usaperfilar(). Cuando ningún formato confirmado nombra un día no hay serie diaria que construir, y entonces la columna se declara encolumnas_omitidasycolumnas_sin_serie_diariaen vez de desaparecer. -
perfilar_por()juntaba los ausentes con el literal"(ausente)"sin decirlo. Una columna que trae ese texto como valor real caía en el mismo grupo que losNA. No se perdía ninguna fila, pero se publicaba un grupo que junta dos cosas distintas bajo una etiqueta. La etiqueta no cambia —es la que documenta la función—; la colisión ahora se declara encobertura_grupos, con cuántas filas aporta cada una. -
perfilar_por()reventaba si un argumento nombraba la columna de agrupación. Esa columna se recorta de cada rebanada antes de perfilar, así quecolumnas_personales,columnas_opcionales,claveoaplicabilidadnombrándola hacían fallar la corrida entera con «nombra columnas inexistentes». Nombrarla es legítimo —es una columna de la tabla— y en el caso decolumnas_personaleses además la única forma de declarar que las etiquetas de grupo llevan datos personales cuando el léxico no reconoce el nombre. Ahora se recorta de lo que se reenvía, que es lo que significa: la declaración vale para la tabla, y en la rebanada esa columna ya no está. -
fecha_nacno se clasificaba como fecha de nacimiento. El patrón abreviaba la primera palabra (f_nacimiento) y no la segunda, así que una de las formas más frecuentes en registros administrativos de la región quedaba sin proteger. Se enumeraron las cinco maneras de escribir lo mismo —entera, primera palabra, segunda, las dos y pegada— en lugar de agregar la que apareció, y la hermana de fallecimiento tenía el mismo hueco por el mismo lado.fecha_fallqueda deliberadamente fuera:falles también «falla» en datos de mantenimiento, y clasificar de más enmascara una columna que no es personal. -
perfilar_por()publicaba datos personales sin decirlo. Las etiquetas de grupo son valores de la columna de agrupación, así que agrupar por una columna de documentos publica los documentos —en los hallazgos y en las dos tablas de cobertura— mientrasperfilar()sobre esa misma columna enmascara su moda. La columna se recorta de cada rebanada antes de perfilar, así que la capa de protección nunca la veía. Las etiquetas no se enmascaran: agrupar por una columna es pedir que la salida se organice por sus valores, y una etiqueta ilegible dejaría el resultado sin eje. Pero ya no ocurre en silencio: se avisa al ejecutar y queda declarado en el atributoetiquetas_personales, con el tipo clasificado y la salida recomendada —agrupar por una columna seudonimizada—. Conproteger_datos_personales = FALSEno se avisa: ahí ya está declarado que se quieren los valores. -
analizar_tiempo()decía haber resumido toda la columna cuando había resumido parte. Los valores presentes que ningún formato confirmado puede convertir —"2022-13-99"y compañía— quedan fuera del resumen temporal, y eso no depende del muestreo; pero sólo se contaban cuando el formato se había descubierto sobre una muestra. Sobre mil valores, la misma columna informaban_fechas_excluidas_parseo = 0conestado_resumen = "calculados"sin muestrear y50con"calculados_sobre_fechas_parseadas"conmuestra = 50, mientrasperfilar()sobre esa misma tabla declaraba los cincuenta: dos salidas del paquete describían la misma columna de dos maneras incompatibles, y la que callaba era la corrida por omisión. Ahora se cuentan siempre. UnNAsigue sin entrar en esa cuenta: es una ausencia declarada, no un valor que el resumen no pudo leer. -
desenlace = "suprimir"se enmascaraba en un lado y se publicaba en el otro. Una regla puede declarar que una medida no debe publicarse; el informe decía[valor suprimido]dos veces y publicaba el valor igual en otra sección del mismo documento. La supresión alcanza ahora al objeto, aprint(), al HTML, al histórico, al tablero, al índice y al archivo guardado. Se conserva la señal —que la medida existe y que fue suprimida— y se oculta el número. -
indice_calidad()ytablero_calidad()no habían heredado la cobertura de métricas. El arreglo que impide publicar un1sobre controles que no corrieron llegaba amedir()yevaluar()y no a estas dos. Las tres salidas llevan ahoracobertura_metricas. -
comparar_equivalencia()no distinguía «son distintas» de «no se pudo comparar». Con la intersección de columnas vacía devolvía cero filas, igual que dos perfiles idénticos. Ahora declaracolumna_solo_en_anteriorycolumna_solo_en_actualencobertura_diagnosticoscon sucomo_resolverlo, y el resumen cuenta comparados y no comparables por separado. - El motivo
faltante_misma_claseya no aparece sobre columnas de clases distintas: dice qué tipo cambió.
La deriva de calidad distingue un cambio en los datos de un cambio en la vara
-
Cambiar el modelo, el perfil de evaluación o la aplicabilidad se leía como cambio en los datos. Subir el umbral de una regla de
0.5a0.9producía una deriva de1a0con severidaderror; cambiar sólo la aplicabilidad daba0.5a1publicado como mejora. Nada de eso ocurría en los datos. Ahora la deriva declara la transición con su propia fila y conserva las comparaciones, para que un cambio de política no oculte una deriva real: el mismo criterio que ya seguíacomparar_perfiles(). -
El histórico ya no compara series de tablas distintas.
acumular_historico()admitía una corrida de una tabla y otra de otra, y la deriva las comparaba sin que nada identificara cuál era cuál. Ahora se separan por su identidad. -
La evidencia conserva los números de fila originales. Tras recortar por
aplicabilidad, las filas se renumeraban sobre el subconjunto y la evidencia nombraba la fila equivocada: con las filas 1 y 3 aplicables informaba1y2. - Una descripción de deriva afirmaba «cambió el resultado» junto a un
delta = 0. Dos corridas idénticas ahora dicen que el resultado se mantuvo. -
Lo que se revisó y no era defecto:
evaluar()promedia las reglas sin ponderar, y así lo declara; noventa y nueve medidas mal y una bien dan0.5porque son dos reglas opuestas, no porque el promedio esconda algo. Se documentó mejor y se conserva el detalle por regla, que es donde se ve la distribución.
El modelo de calidad ya no publica el padrón ni un 1 que no midió
-
Cuarta puerta de fuga, y venía encendida. Las métricas referenciales informan, ante un fallo, cuál era el candidato más cercano y a qué distancia —y eso es justo lo que vuelve accionable el hallazgo—. Pero incrustaban el valor real del referencial en
objeto_medible: sobre un padrón de personas, el documento y el nombre completo, propagados ahistorico_calidad(), que la documentación promociona como exportable conwrite.csv(). Ahora el valor sale enmascarado y la señal se conserva:{candidato_referencial=[valor protegido]; distancia=0.0296}. Un referencial sin datos personales mantiene su evidencia completa. -
Una métrica que no se puede evaluar ya no desaparece. Sobre una columna sin ningún valor, la métrica de formato se esfumaba de la medición y la evaluación publicaba
1—calidad perfecta— sobre los controles que sí habían corrido. Ahora queda registrada en la cobertura de métricas con su motivo, y la regla y el perfil informanNAen lugar de un número que no midió lo que dice. Una medición completa sigue dando su resultado sin filas de cobertura.
El plan dice en qué unidad cuenta, y aplicar() no miente sobre lo que hizo
-
Una acción podía reportarse
ejecutadasin hacer nada. Cuando la columna determinante de una dependencia funcional es dato personal, el plan enmascara su mapa —y hace bien: ese mapa lleva documentos—, pero la imputación cotejaba los datos contra las claves enmascaradas, que nunca coinciden. El registro decíaejecutada,n_cambiadas = 0, sin error, y elNAseguía ahí. Ahora la dependencia se resuelve sobre los datos que recibeaplicar(), así que la imputación funciona y el mapa sigue sin viajar en claro. -
Un efecto nulo ya no se declara
ejecutada. Una acción que prometía cambiar filas y no cambió ninguna quedafallidacon su motivo. Que lo prometido y lo hecho no coincidan tiene que ser visible. -
El plan declara
unidad_conteo.n_afectadasheredaba el número del hallazgo sin heredar su unidad, así queconvertir_minusculasanunciaba3—valores distintos en colisión— y cambiaba 90 filas. El número no cambia; ahora dice en qué unidad está, yprint()yguiar_limpieza()lo muestran, que es donde se lee antes de decidir. -
La clave de un
data.tablese suelta cuando la acción rompe su orden. El resultado declaraba estar ordenado por una columna que acababa de cambiar.data.tablesuelta la clave sola en ese caso; el paquete hacía lo contrario.
perfilar_dbi() compara sus dos bloques y declara la divergencia
- El objeto trae
resumen_tabla, que calcula el motor con SQL, yperfil_muestra, que esperfilar()sobre una muestra. Calculaban la misma cantidad por dos vías y publicaban las dos sin compararlas. Ahora, cuando ambos existen, se cruzan las once métricas comunes y toda diferencia que supere la tolerancia deja una filadivergenciaenresumen_tabla$coberturacon los dos valores, la cobertura de la muestra y las causas posibles. - No se elige un ganador. El paquete no sabe cuál de los dos es la verdad, y decirlo sería inventar; lo que sí puede hacer es no callar que no coinciden. Los agregados del motor y el perfil de la muestra no cambian.
- Una muestra que no cubre la tabla no produce divergencias: la comparación distingue la diferencia esperable por muestreo de la que no lo es.
- Sale de tres casos medidos con causas distintas y un mismo síntoma:
NaNeInf, que SQL no trata como R —once métricas discrepaban—; la cancelación de coma flotante, donde{1e16, 1, -1e16}da media0en el motor y la verdad es 1/3; y un defecto dePERCENTILE_CONTen MariaDB sobreDECIMALgrande, que publicabamediana = 1e+08mientras la muestra decía1,23e+19—once órdenes de magnitud, las dos en el mismo objeto y sin una nota—. - Parchear cada causa habría sido una carrera perdida: el catálogo de motores por tipos no cierra. Cruzar los dos bloques las cubre a todas, y a las que todavía no aparecieron.
Tres puertas por las que se escapaba un valor protegido
Una columna clasificada como dato personal se protege: la moda sale como [valor protegido], el mínimo y el máximo quedan en NA. Tres salidas publicaban igual un valor real, incluso con proteger_datos_personales = TRUE pedido explícitamente. En las tres se conserva la señal: lo que no puede salir es el valor, no el aviso.
-
posible_ausencia_estructuralpublicaba un umbral que era un número de documento real, en la evidencia y en la sugerencia —aplicabilidad = list(x = ~ documento < 70231469)— y llegaba al informe HTML. Ahora el hallazgo se sigue emitiendo y oculta el umbral y la fórmula cuando la columna determinante está protegida. -
guiar_limpieza()imprimía filas enteras por consola. Sus «Ejemplos reales» se recalculan sobre los datos crudos que recibe la función, así que no pasaban por la capa que protege el perfil: confilas_duplicadasmostrabadocumento=,correo=ytelefono=con sus valores. Ahora enmascara los valores de las columnas protegidas y conserva la fila, el grupo y qué columnas participan, que es lo que hace falta para decidir. -
bbox_*publicaba las coordenadas de una geometría protegida. Para una columna de domicilios, el rectángulo delimitador es una divulgación geográfica. Los cuatro campos quedan enNAybbox_alcancedeclarano_publicado_por_geometria_protegida; el CRS, el tipo, la dimensión y los conteos se conservan, porque no dicen dónde vive nadie.
Lo sostiene un barrido que recorre los tipos de hallazgo y las salidas —consola incluida, capturando también lo que emite cli— y que comprueba primero que cada uno se haya emitido: un caso donde la regla no dispara da el mismo resultado que uno donde funciona bien.
Los dos conteos de un resumen parcial cuentan cosas distintas
-
n_fechas_excluidas_granularidadcuenta sólo las fechas que quedaron fuera por ser de granularidad mes. Antes, cuando había muestreo, también sumaba los fallos de conversión: una columna con noventa fechas de sólo mes y diez ilegibles publicaba cien «excluidas por granularidad», y quien lo leyera buscaba una granularidad que explicaba noventa de esos cien. -
n_valores_excluidos_resumenes el campo general —el mismo que ya usaban 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. -
Con muestreo, la atribución por causa se informa
NA. Ese conteo se deriva de los formatos detectados en la muestra mientras el resumen recorre la columna entera: publicaba4donde eran90. Atribuir la causa exigiría rehacer la detección sobre todo, que es el costo que el muestreo existe para evitar, así que se informaNAy nunca cero. El total sin atribuir sigue estando. - La fila
resumen_cuantitativodecobertura_diagnosticosse emite ahora siempre que algún valor presente quede fuera del resumen, para fechas y para números por igual y haya muestreo o no. Leía el conteo de granularidad y con este cambio habría quedado muda justo en el caso muestreado.
Una política declarada no se disfraza de cambio en los datos
-
comparar_perfiles()compara ahora la política de centinelas de las dos corridas y, si difiere, agrega una filaconfiguracion_sentinelas_numericosde severidaderror. Antes, el mismodata.framebyte a byte perfilado con dos políticas distintas producía seis filas de deriva —una de ellas «Cambió el rango observado de la columna»—, y quien seguía la calidad de una carga mensual concluía que le habían llegado datos distintos. - Las comparaciones se conservan. El otro camino posible era declarar la incomparabilidad y no comparar, como ya se hace con la configuración de patrones; se descartó porque dejaría ciega la detección de deriva verdadera en todas las corridas posteriores al cambio de política, que es peor que el problema que resuelve. La fila declara la transición y la evidencia dice que las diferencias pueden atribuirse a ella.
perfilar_dbi() dice qué políticas no llegan al motor
-
sentinelas_numericos,aplicabilidadycolumnas_opcionalesson ahora argumentos explícitos deperfilar_dbi(). Entraban por...y se aplicaban sólo aperfil_muestra, así que una misma llamada podía publicarmedia = 1045.09en el resumen del motor ymedia = 50.21en el de la muestra sobre las mismas filas, sin que nada lo dijera. - Cuando alguna de ellas se usa,
resumen_tabla$coberturarecibe una filadegradadoque explica que los agregados SQL no honran esa política y que para ese fin sirveperfil_muestra. El resumen del motor no cambia: traducir las políticas al SQL de cada dialecto es otro trabajo, y mientras no esté hecho el paquete lo declara en vez de callarlo.
El período AAAA-MM es una fecha de granularidad mes
-
%Y-%m,%m/%Yy%Y/%mse reconocen como períodos mensuales. Antes la granularidad"mes"salía sólo del mes escrito en letras —%B %Y,%b %Y—, que es la forma rara;AAAA-MMes la forma ISO y la que producen los exportes y las consultas agrupadas por período, y no se reconocía: cien períodos entre novecientas fechas con día desaparecían del resumen y el mínimo publicado se quedaba en el año equivocado, conestado = "calculados". - Entran por el camino que ya existía y no se inventa nada: los períodos no se convierten a fecha —asignarles el día 1 sería inventar un dato—, se cuentan en
n_fechas_excluidas_granularidady el estado baja a"calculados_sobre_dias", o a"granularidad_incompleta"si son la columna entera, dondeminimo_fechaymaximo_fechaquedan enNA. -
No entra
%Y%m. Seis dígitos sin separador son un entero, y una columna de202305se sigue perfilando comoentero: robarle esos valores a la lectura numérica habría sido peor que el defecto que se arregla. - Los períodos no se cruzan con los sufijos de hora:
"2023-05 14:30"no existe.
Un resumen parcial lo declara siempre, no sólo al muestrear
- Los valores presentes que la conversión de texto a número no puede leer se cuentan en
n_valores_excluidos_resumenhaya muestreo o no, y el estado baja a"calculados_sobre_valores". Antes el conteo estaba atado al muestreo: la misma columna informaba cero valores excluidos con estado"calculados"sin muestrear, y cien con"calculados_sobre_valores"conmuestra = 50. Los cien quedaban afuera en los dos casos; lo único que cambiaba era si se decía.
Un centinela que el usuario declara vale lo mismo que un NA
-
sentinelas_numericosdistingue la lista por omisión de una política que el usuario reemplaza. Los valores que alguien declara salen demedia,mediana,minimo,maximo,desvio, los conteos de signo yn_outliers;n_valores_excluidos_resumenyestado_resumen_cuantitativodeclaran ese alcance. La lista por omisión —-9,-99,-999,-9999y999— sigue informándose comofaltantes_disfrazadosy no se retira de los resúmenes, porque999también puede ser una edad, un código postal o un año, y actuar sobre una sospecha cambiaría números que nadie pidió cambiar. -
Esto cambia resultados publicados para quien pasa su propia lista. Sobre mil filas —950 valores alrededor de 40, treinta
9999y veinte9998— la media declarando ambos códigos pasa de 537,9 a 39,96, que es la verdad. - El defecto se vio comparando tres formas de decir lo mismo:
aplicabilidady unNAescrito a mano cambiaban la media, ysentinelas_numericos—que la documentación llama «la política completa»— no. Las tres coinciden ahora. -
moda,frecuencia_moda,n_distintos,tasa_distintosylongitud_*siguen describiendo la representación almacenada, igual que ya contaban unInfque los estadísticos excluyen. Por esomodapuede quedar fuera del rango deminimoymaximo: no es una contradicción sino la diferencia entre describir lo guardado y resumir lo que vale como dato, y los conteos dicen cuánto separa a los dos. -
La severidad seguía la misma confusión. Con el umbral de error en 0,4, seiscientos
NAde mil dabanerrory los mismos seiscientos declarados como centinela dabansospechoso. Ahora la rebaja se aplica sólo cuando toda la ausencia se apoya en códigos que el paquete supuso, que es el caso para el que se escribió. - El hallazgo
posible_centinela_numericosugería agregar el valor asentinelas_numericos«para que se cuente como faltante» y callaba lo que pasaba con los números. Ahora dice qué campos lo dejan afuera y cuáles siguen contándolo.
Un resumen calculado sobre parte de la columna lo dice
-
Con
muestra, los formatos y tipos se descubren sobre la muestra pero la conversión corre sobre la columna entera, y los valores en otro formato quedaban afuera sin que nada los contara: el mínimo de fechas podía publicar 2024-01-01 sobre una columna que llega a 2022, conestado_resumen_cuantitativo = "calculados"ycobertura_diagnosticosvacía. - Ahora esos valores se cuentan —
n_fechas_excluidas_granularidadpara fechas,n_valores_excluidos_resumenpara números de texto—, el estado deja de decir que el cálculo fue completo, ycobertura_diagnosticosagrega una filaresumen_cuantitativo. Sinmuestrano cambia nada. -
analizar_tiempo()aplica el mismo criterio conn_fechas_excluidas_parseoyestado_resumen, y las relaciones de orden entre columnas no comparan una columna cuya conversión quedó parcial.
Las figuras del banco de rendimiento salen de un CSV
- Cuatro figuras nuevas en el README —escala, estimación previa, cardinalidad y pérdida del tamiz LSH— dibujadas con R base por
benchmark/graficar_figuras.Rdesde los CSV debenchmark/datos/, que dejanbenchmark/medir_figuras.Rybenchmark/perdida_lsh.R. Ninguna cifra vive en un guion ni en una transcripción de consola: cada figura lleva al pie el commit, la fecha y la máquina, el graficador se detiene si los CSV mezclan commits, y al final imprime las cifras que el README cita en prosa para que otra corrida las desmienta con undiff. - El medidor firma el commit con
+suciosi tienen cambios sin commitear las rutas que determinan el resultado —R/,DESCRIPTION,NAMESPACE,src/,inst/y el propio guion que mide—, comprueba que cada proceso hijo carga la misma instalación que midió el padre, y se detiene si las tres corridas de una configuración no dan los mismos candidatos, pares, recall y estimación previa. -
El commit firma el árbol, pero lo que se mide es la instalación, y nada obligaba a que fueran el mismo código: una corrida sin
R_LIBSmide la biblioteca por omisión —que puede ser de hace días— y firmaba igual el commit deHEAD. Ahora los dos medidores se detienen si el selloBuiltde la instalación es anterior al último commit de las rutas firmadas. Es condición necesaria y no suficiente, y así está dicho: atrapa la instalación vieja olvidada, no a quien quiera engañarla.LUPA_FIGURAS_SIN_GUARDA_BUILT=1deja medir a sabiendas y firma los CSV con+singuarda.entorno.csvpublica además de qué biblioteca salió lo que se midió. - La firma se firma a sí misma. Con
benchmark/_firma.Rfuera de las rutas vigiladas bastaba con sacarle la rama+suciopara que un árbol sucio se firmara limpio: el archivo que decide el veredicto era el único que el veredicto no cubría. Entra también.Rbuildignore, por una razón que se demostró construyendo el tarball: agregándole una línea sin commitear se saca del paquete un archivo deR/que sí está commiteado, y quien lo instale mide un paquete al que le falta código del commit firmado. - Sin
git, la columnacommitqueda enNAcomo antes, pero ahora la corrida lo dice en voz alta en vez de dejarlo sólo en el CSV. - El graficador se detiene ante un CSV que lo haría afirmar lo que el dato no dice: un valor no finito en una columna dibujada —el título salía «entre NA % y NA %»—, una proporción fuera de
[0, 1]—«150 %»—, más de una fila donde dibuja una sola,hilos.csvcon tamaños distintos o repetidos, o un nivel de cardinalidad sin descripción. Salieron de darle CSV rotos de a uno, y cada mensaje nombra el archivo, la columna y la fila. - El título de la figura del tamiz da el rango de todo lo que la figura dibuja, no el de las medias: decía «entre 51 % y 76 %» con puntos en 48 % y en 80 % al lado.
- La tabla de hilos de la viñeta
escala-y-duplicadosahora sale debenchmark/datos/hilos.csv: la anterior venía de un padrón «que no se distribuye» y era este mismo padrón sintético. Con dos hilos 153,20 s; con dieciséis, 85,01 (0,55×); pasados dieciséis la ganancia queda dentro del ruido entre corridas. La ganancia sobre el otro padrón se corrige de 12 % a 10 % (290,42 s frente a 324,36), que es lo que su propia tabla decía.
La tasa de doscientos caracteres, medida otra vez
-
supuesto_costoy la viñetaperfilar-una-basedecían «unos 80.000 pares por segundo» sobre valores de doscientos caracteres. Una corrida nueva debenchmark/medir_costo_texto.Rsobre este mismo código da 75.705 y 76.610, y la entrada vieja de este archivo decía 70.000: las tres son corridas distintas de la misma medición. Ahora las dos dicen «entre 70.000 y 80.000», que es la banda que las corridas sostienen, en vez de la punta más favorable.
Fechas personales de fallecimiento
- Se reconoce
fecha_fallecimientopor nombre, se protegen sus momentos y se informafecha_fallecimiento_fuera_rangosin publicar las fechas observadas.
Ronda de arreglos DBI contra bases reales
- La muestra saturada por spool (
spool_sesion_clienteconfraccion = 1) clasificamemoria_trabajocomocreciente, igual que la tabla completa, en vez de dejarloNA. -
rss_maximopublicaNAcuando ningún evento del vigilante trae una lectura finita, en vez de avisar y publicar-Inf. - La documentación de
bloque_filasdice queperfil_muestrasigue siendo la muestra diagnóstica acotada pormuestra,max_celdas_muestraymax_bytes_muestra, y que la cobertura se lee enmeta$bloques$filas_vistas.
El resultado deja de depender de la configuración regional
iconv("ASCII//TRANSLIT") era una de las dependencias de plataforma, no la única. Estas son de la misma familia y todas se reprodujeron antes de tocarlas.
-
tolower()no pliega los acentos fuera de un locale UTF-8. Connormalizacion(acentos = FALSE, minusculas = TRUE)—un perfil documentado—, bajoLC_CTYPE=C«CAFÉ» y «café» dejaban de ser la misma clave y el par duplicado se perdía en silencio. Ahora el plegado de caja usa un mapa explícito de mayúsculas latinas acentuadas, enR/pliegue-caja.R, que conserva el acento y sólo cambia la caja. Comprobado comparando puntos de código: bajoCy bajoes_UY.UTF-8el resultado es el mismo. Al auditar el mapa entrada por entrada apareció que faltaba justoU+0130, la I mayúscula con punto —la única mayúscula latina sin cubrir, y la que el locale trata distinto—: sin entrada propia se plegaba aien un locale UTF-8 y quedaba intacta bajoC. Y.normalizar_nombre_columna()seguía dependiendo del locale por un problema de orden: bajaba la caja antes de transliterar, así que bajoLC_CTYPE=Clas mayúsculas acentuadas llegaban intactas y el filtro[^a-z0-9]las borraba —Ñandúquedaba enanduyVÍA_1enva1—. - Los dos mapas cubren ahora lo mismo y el alcance está escrito. Faltaban la doble ese mayúscula —que estaba en el mapa de plegado y no en el de transliteración, así que la letra desaparecía del nombre—, las marcas combinantes —sin ellas la forma descompuesta de un nombre no colapsaba con la precompuesta— y un puñado de Latin Extended-B que aparece en nombres europeos. Fuera de Latin-1, Latin Extended-A y ese puñado, el carácter se conserva y lo decide quien consume: eso queda dicho en el código en vez de suponerse.
-
El «orden canónico —alfabético—» no era canónico:
sort()sobre texto usa la intercalación del locale, y ese orden desempata qué pares sobreviven al recorte demax_resultados. La misma corrida sobre los mismos datos podía devolver pares distintos según la máquina. Ahora usa.ordenar_por_bytes(), que el paquete ya usaba en otro lado, y el comentario dejó de afirmar algo falso. - Las variantes de mayúsculas de una columna se detectaban con el mismo
tolower()dependiente del locale. - La consulta al catálogo de Oracle elevaba identificadores con
toupper(): con un locale turco pedíaMİ_TABLAen vez deMI_TABLA. Ahora eleva con reglas ASCII explícitas, y sólo los identificadores. - Los textos de corte de ausencia estructural salían con coma decimal bajo un
LC_NUMERICque la usa.
Las pruebas nuevas fijan el locale dentro de la prueba y lo restauran; si la plataforma no acepta el locale pedido, se saltean diciendo por qué.
Documentación que se puede desmentir
- Los dos README decían «43 tablas limpias» donde son 31: la cifra vieja quedó de cuando el conjunto de control era más grande, y el mismo README ya decía 31 en su tabla de evidencia. Ahora la prosa nombra la prueba que la fija.
- La lista de nombres de
tipo_hallazgoque publican los README no la vigilaba ninguna prueba —que es exactamente como envejeció la anterior—. Ahoratest-vocabulario-publicado.Rcomprueba que la cifra coincida con la lista, que los dos README publiquen la misma, y que cada nombre siga existiendo en el código. - Siete
\valuedescribían el retorno como «un objeto de clase X». Ahora dicen qué trae ese objeto y qué no hace la función. -
DescriptionexplicaAGESIC,ISO/IECyCEA/CEPAL, que el CRAN Cookbook pide expandir.
Geometrías con Z/M o con SRID mixto: se acabaron los falsos positivos
Medido contra un PostGIS 3.4.3 real, sembrado a propósito con lo que la base real de la ronda anterior no tenía: GEOMETRYCOLLECTION anidada y vacía, POINT Z, POINT M, POINT ZM, vacías legítimas mezcladas con no vacías, y SRID 31981 junto a 4326.
- La vía DBI declaraba fuera de dominio geometrías válidas: una de cuatro en la columna con Z/M, dos de tres en la de SRID mixto, cinco de siete en la que mezclaba todo. Dos causas: el dominio se transformaba sin retirar Z/M, y una columna con SRID mixto se transformaba entera con un solo CRS.
- Ahora las dimensiones Z/M se retiran antes de transformar al sistema geográfico, y se siguen declarando en
dimension_geometria. - El SRID se lee de cada EWKB:
crs_declaradopublica la mezcla —"31981, 4326"— y el dominio se evalúa por grupos de SRID.crs_geograficoquedaNAcuando hay mezcla, porque esa columna no tiene uno solo. - El arreglo no silencia nada: en los cinco casos ninguna geometría buena queda rechazada, las tres vacías legítimas se siguen contando como vacías y la auto-intersección se sigue informando como inválida.
-
sf::sfcno puede representar una columna con SRID mixto —tiene un CRS por columna—, así que ahí la vía DBI es la que conserva lo que declaró cada fila.
Las guardas geométricas no le entregan datos corruptos a GDAL
- La plausibilidad WKB exige también el largo mínimo por tipo (un punto lleva 16 bytes de cuerpo; el resto, su conteo): un encabezado válido con cuerpo trunco ya no llega a
sf. Un GDAL real de win-builder reventaba —violación de segmento, no error— con ese trunco, ytryCatchno atrapa eso. - El WKT gana su propia guarda aritmética (palabra de tipo,
SRID=opcional, sólo números y paréntesis balanceados): el texto corrupto declara la pérdida sin quesflo reciba, con el mismo resultado público de siempre.
Ronda de arreglos del núcleo
- La protección de datos personales protege las dos representaciones de la media (
mediaymedia_fecha), conserva los desvíos y declara también la supresión de la moda en el detalle de protección. -
comparar_equivalencia()omite campos protegidos y los declara encampos_protegidos; también omite magnitudes numéricas cuando sólo uno de los perfiles es temporal y conserva el motivo del cambio de esquema. - Los acumuladores por bloques rechazan entradas no atómicas o con dimensión en el mapa de distintos, con estado
no_disponibley motivo explícito.
Ronda de arreglos DBI
- La via por bloques inicia el mapa central cuando se pide moda o mediana sin publicar
n_distintos; aplicapolitica_costo = "por_cardinalidad", registradbfetch_bloquescomo estado acotado y audita filas, orden y pasadas. - La atribucion de derrames de PostgreSQL empareja
pg_stat_statementsporqueryid, y los avisos de estimacion declaran el origen de su denominador aun cuandovalidosno fue solicitado. - Los spools reciben identificadores propios de cada materializacion y los motivos de muestras vacias publican el metodo que realmente corrio.
Integración DBI — spool y contratos de muestra_motor (Etapa I2)
-
universo = "muestra_motor"ejecuta una sola selección y la materializa en un spool externo de sesión cliente. El trailer verificamuestra_id,snapshot_id,orden_id,n_filas,bytesy checksum en la relectura; las pasadas leen ese spool y nunca vuelven a muestrear el motor. - El spool no escribe en la conexión DBI ni crea objetos temporales del motor.
meta$materializacionpublica backend, versión, checksum, bytes, presupuesto y estado. Un exceso medido antes de escribir cada chunk publicaspool_presupuesto_excedidoymuestra_inestable:presupuesto_materializacion; no se entrega una muestra híbrida. -
perfil_muestra$meta$filas_analizadas,hallazgos,cobertura_diagnosticosymeta$origen_dbi$muestreoforman un contrato explícito. Las familias que no se evaluaron quedan como filas de cobertura; la impresión remite a esa cobertura. El identificador heredado de consulta ahora se llamaid_consulta, sin alias;muestra_idqueda reservado a la relación materializada. - El punto de cruce medido en PostgreSQL 16 fue 5,6 s con spool frente a 3,3 s reordenando en cada pasada para 500.000 filas (10.000/100.000/500.000: 0,448/1,684/5,598 s frente a 0,814/2,178/3,265 s). La elección del spool responde a identidad y reutilización, no a una promesa de velocidad.
Integración DBI — fuente por bloques (Etapa I1)
-
perfilar_dbi(..., bloque_filas = n)incorpora una vía optativa para recorrertabla_completacon un únicodbSendQuery()y sucesivosdbFetch(n). Los acumuladores conservan ordinales globales y el resumen publicaalcance, bloques recorridos, bytes retenidos y eventos del vigilante. - El preflight resuelve clave primaria, localizador o
ROW_NUMBER(), publica el método, la collation efectiva y su determinismo. Una collation textual no determinista degrada el orden; DuckDB queda explícitamenteno_disponible:dbfetch_no_incrementalsegún la matriz por driver. - La identidad de una segunda pasada por localizador se comprueba contra los ordinales de la primera. El resumen de bloques conserva estadísticos finitos y sus contadores separados de
NA,NaNe infinitos; un resumen SQL que no pueda leer+/-Infpuede divergir por la representación del controlador.
Estimacion de derrame de moda y mediana en DBI
-
plan_perfilado_dbi()yperfilar_dbi()publicanestimacion_derrame_modayestimacion_derrame_mediana. La moda deriva su metodo del planEXPLAIN (FORMAT JSON, COSTS OFF)sinANALYZE; la mediana usa la huella de decision del sort, con pisos de 32 bytes para tipos fijos y 42 paranumeric. La consolidada decide por el maximo de columna y publica la suma de tapes solo comoestado_io_total_bytesinformativo. - Cuando se solicita
validos, la meta y los avisos usan eln_validosmedido por los agregados planos y conservan la cifra de catalogo. Sin esa familia, declaran el repliegue al catalogo. El canal medido incluye moda, mediana y consolidada: normaliza literales de forma posicional y publicallamadas_en_ventanacuando agrega llamadas concurrentes.
Mediana y conteos bajo muestreo DBI
-
perfilar_dbi(universo = "muestra_motor")calcula la mediana sindos_consultascuando el dialecto requiere la CTE de ventanas, y publica los conteos de la muestra con sus denominadores locales.TABLESAMPLE SYSTEMdeclara su sesgo por bloques; el fallbackNEWID()queda gobernado por una politica de costo y se rechaza con un motivo estable cuando excede el presupuesto.
Memoria de trabajo en la auditoría DBI
-
resumen_tabla$sqlagregamemoria_trabajo, derivada del estado de medición, del alcance efectivo y del método resuelto. Distingue trabajocreciente,acotadoy filas sin medición (NA), incluida la muestra saturada, cuyo tope vacuo se clasifica por método.
Comparación de perfiles y estado del tipo inferido
-
comparar_equivalencia()compara campos compartidos con un registro fijo de ejes y una tolerancia explícita del llamador, sin convertir sus resultados en decisiones del paquete. - El perfil por columna publica
estado_tipo_inferidocomoconfirmado,candidatooNA; el resumen impreso anota sólo los tipos candidatos.
Ejecutor interno por bloques — Etapa 4: LSH externo
- Se incorpora el ejecutor LSH en dos fases con runs ordenados, diccionario externo por merge-join, firmas y cubetas derramadas; conserva los pares al cruzar bloques y publica el desglose de memoria residente, RSS, factor pico y piso de fila.
- Si falta backend de derrame, snapshot u orden estable, LSH queda
no_disponiblesin degradarse silenciosamente.
Ejecutor interno por bloques — Etapa 3: índice
- Las familias de trazabilidad y ejemplos conservan ordinales globales: los índices se recortan al finalizar y los primeros valores únicos se resuelven sobre el orden de la fuente, no por bloque.
- La muestra sistemática por bloques calcula una sola vez sus índices globales y aplica la intersección con cada intervalo; reproduce
.muestrear_vector()para 1, 2, 7 y 31 bloques. - Los sobres de índice publican
orden,snapshot, topes y truncamiento; la integración DBI y suORDER BYqueda reservada para la Etapa 4.
Ejecutor interno por bloques — Etapa 2: valor
- Las medianas y cuartiles se reconstruyen desde el mapa central ponderado, sin expandir frecuencias; reproducen
type = 7ymedian()bajo el filtro explícitois.finite. - Outliers, centinelas, huecos de secuencias y constantes proporcionales aritméticas tienen acumuladores, segundas pasadas y topes visibles. Si el multiset no cabe, publican
no_disponiblecon motivo y resolución sugerida. - La aceptación cubre 1, 2, 7 y 31 bloques, la identidad paramétrica, valores infinitos/ausentes, truncamiento y las barreras finales del vigilante.
Ejecutor interno por bloques — Etapa 1
- Se incorpora el ciclo interno
iniciar/absorber/fusionar/finalizar, con medición de bytes residentes y sobre uniforme de resultado. - Conteos, mínimos, máximos, media, desvío y longitudes admiten partición y fusión; el mapa central de distintos conserva la igualdad de R, incluidos
NA,NaN,+0/-0einteger64. - Se migran moda y cardinalidad al mapa central cuando está completo, con cota visible al alcanzar el límite. El vigilante registra barreras de bloques y de finalización, incluida la presión artificial de memoria.
- La aplicabilidad rechaza temprano predicados que dependen de estadísticos globales; los predicados por fila conservan su máscara entre particiones.
perfilar_dbi() separa universo, métricas y estrategias
- Se retira el argumento
mododeperfilar_dbi()yplan_perfilado_dbi(), junto conmeta$modo. La firma ahora separa el alcance (universo), las métricas (metricas) y las estrategias de cada agregado; el plan y el perfilado comparten el mismo contrato. - Traducción de los presets anteriores:
exactoson los valores por omisión (universo = "tabla_completa", todas las métricas y estrategias exactas);seguroequivale ametricas = c("validos", "basicos", "desvio");conteosequivale ametricas = "validos";muestreadoequivale auniverso = "muestra_motor",muestra_motor = nymuestra = n; yaproximadose traduce comoestrategia_mediana = "aproximada_motor", sin forzar el catálogo ni apagar el atajo estructural de otros ejes. La procedencia aproximada de distintos se pide aparte conestrategia_distintos = "aproximada_motor". -
estrategia_medianadescribe ahora el orden de la sonda: primero intenta una forma nativa exacta consolidada, después una exacta por columna y sólo al final una función aproximada nativa. El método y el estado publicados son los que efectivamente corrieron: una mediana exacta quedacalculado, conerror_esperado = "no_aplica", aunque se haya solicitado la estrategia aproximada; sólo una aproximación ejecutada quedaestimado. -
universo = "muestra_motor"desactiva la inferencia estructural de cardinalidad por clave primaria: el atajo no puede convertir una muestra en una medición del universo completo. Además, esa combinación rechaza tempranoestrategia_mediana = "aproximada_motor", porque la muestra ya es una aproximación del universo. -
politica_costoacepta sólo"todas"y"por_cardinalidad"; desaparecen los alias"ninguna"y"cardinalidad".
La sonda de mediana consolidada conserva el error del motor
- La razón de no activar la mediana consolidada incluye el mensaje de la sonda rechazada, y la documentación explicita el requisito de compatibilidad >= 110 de SQL Server.
La cobertura DBI espeja el tope de muestra
- Cuando
max_celdas_muestraomax_bytes_muestrarecorta una muestra DBI, la misma fila y el mismo motivo quedan enperfil_muestra$cobertura_diagnosticosyresumen_tabla$cobertura.perfil_muestra$meta$tope_que_mandodeclara si mandaronceldas,byteso la propiamuestra, y el texto queda junto a esa declaración.
El plan DBI aprovecha reltuples como estimación
- En PostgreSQL, cuando la preparación ya leyó la jerarquía del catálogo, el plan reutiliza
pg_class.reltuplespositivo para publicar filas, magnitud y proyecciones de trabajo de moda/mediana. Todo queda rotulado como estimación de catálogo y no como medición;reltuplescero o negativo conservasin dato. - La referencia temporal incluida en los supuestos del plan queda explícitamente identificada como proveniente de otras corridas, no como el tiempo de la tabla planificada.
Señal de concentración modal
-
perfilar()agregavalor_concentrado, la señal M2 seleccionada por la medición de concentración. Sólo considera columnas numéricas con al menos 20 valores válidos y 10 valores distintos; emite severidadsospechosocuando la 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 válidos. La evidencia publica el valor, las dos frecuencias, el cociente y la fracción. - La elegibilidad no agrega ruido a
cobertura_diagnosticos: una columna no elegible simplemente queda fuera de esta señal. La documentación declara sus puntos ciegos medidos: no detecta concentraciones menores al 15 % y los empates naturales en enteros pequeños pueden dejar corto el cociente.
Los bytes WKB basura no llegan a GDAL
- Las columnas
rawque no tienen un encabezado WKB plausible ya no se envían asf; esto evita el crash de GDAL observado en win-builder release (R 4.6.1, Windows) y declara la pérdida para WKB declarado.
Avisos DBI para moda y mediana
-
perfilar_dbi()agregaavisar_costo_modaconumbral_segundos_aviso_modayavisar_costo_medianaconumbral_segundos_aviso_mediana, encendidos por omision y con umbral de 30 segundos. Los avisos se emiten antes de pagar la consulta proyectada. - La moda se proyecta por cardinalidad y la mediana por filas. Las referencias locales se miden durante la corrida; cuando no hay una medicion local de mediana se usa, declarada como referencia de otra corrida, la tasa de 68 ms por millon de filas. La cardinalidad faltante para moda queda declarada y no se inventa.
- Si la consulta inicial que obtuvo las filas fue medida y da una cota mayor que la referencia bancaria, se publica como cota de lectura observada y se usa para no subestimar una mediana grande; no se presenta como medicion de mediana.
- Las proyecciones quedan separadas en
meta$costo_modaymeta$costo_mediana, junto con el ya existentemeta$costo_distintos.
Una muestra vacía no publica métricas no medidas
- Cuando la consulta de
universo = "muestra_motor"devuelve cero filas, las métricas de alcancemuestraquedan enNAy con estadono_disponible; el motivo nombra la muestra vacía.nconserva el conteo de la tabla completa ycoberturamantiene el aviso de que la consulta de muestra devolvió cero filas. - No se publica
0enn_validoson_distintos, ni se dispara la cascadasin_valores: una muestra vacía no permite concluir que la columna esté vacía. Una muestra no vacía con todos sus valores nulos conserva el estado medido/estimado que corresponde.
La política de costo separa moda y mediana
-
politica_costo = "por_cardinalidad"ahora aplicaumbral_cardinalidadúnicamente a la moda. El valor por omisión baja de0.95a0.5, el primer punto medido donde la moda se aleja de su piso de costo; la mediana se conserva porque su costo queda plano frente a la cardinalidad y depende del número de filas. - Esto cambia el comportamiento de quien ya pasaba
umbral_cardinalidadpara omitir ambas métricas: esas llamadas ahora conservan la mediana bajopor_cardinalidad. No se agrega un segundo umbral porque no hay evidencia para omitir la mediana por proporción.meta$decisiones_costodeclara la razón de conservar u omitir moda y mediana por separado, y ambos README y la documentación de la API describen el alcance del argumento.
Topes de la muestra en memoria y DBI
-
perfilar()yperfilar_dbi()aceptanmax_celdas_muestraymax_bytes_muestra, con los mismos valores por omisión:1.000.000celdas y512 MiB. En DBI sólo limitan el bloqueperfil_muestra; los agregados SQL conservan su alcance. - En DBI el tope de celdas se resuelve antes de leer con el conteo de filas y el ancho del esquema. El de bytes hace una sonda de hasta 100 filas y fija el límite final en SQL o
dbFetch(n), sin traer todo para recortarlo en R.plan_perfilado_dbi()anticipa el recorte de celdas y la sonda de bytes. - La reducción se declara en
cobertura_diagnosticoscon celdas o bytes observados, umbral, motivo y cuál tope mandó. Con ambos topes enInfno se declara un recorte.
estrategia_distintos = "catalogo" en PostgreSQL
-
perfilar_dbi()yplan_perfilado_dbi()leenpg_stats.n_distincty publican sus resultados como estimaciones de catálogo en los modos que miden la relación entera (exacto,seguroyconteos). Los valores negativos se convierten con la suma depg_class.reltuplesde la jerarquía que lee una consulta sinONLY; si la relación tiene descendientes se eligeinherited = TRUEy, sin hijas, la única filaFALSE. Enmuestreadoyaproximado, la estrategia quedano_disponible: el catálogo describe la relación entera y la corrida mide un subconjunto, así que no se publica la cardinalidad de un universo como si fuera la del otro. Si faltaANALYZE, el denominador de la jerarquía no es utilizable o hay ambigüedad, el resultado quedano_disponibley no se reemplaza por cero.
Validación de claves
- Se cierra el pendiente de una política separada con
catalogo,reutilizar_recorridoyexacta: la API vigente ya separa la procedencia de cardinalidad (estrategia_distintos) de la política de costo (politica_costo), por lo que no se agrega un selector de clave duplicado.
El plan no repite sus supuestos
- Con la magnitud del trabajo desconocida,
print()del plan mostraba dos veces los mismos dos párrafos —el supuesto del trabajo y las referencias medidas—, porque dos ramas los imprimían y con esa magnitud corrían las dos. Quedó una sola, la que corre para toda magnitud, y una prueba cuenta las apariciones.
La clave que se publica es la de la tabla que se midió
- Con un nombre sin calificar,
perfilar_dbi()publicaba la clave primaria de una tabla homónima de otro esquema cuando la medida no declaraba ninguna. En SQL Server, sobre una tabla de columnasy, dato, llegaba a publicar clave primaria en una columnaxinexistente ahí. El esquema se le pregunta ahora al motor con sus mismas reglas de resolución, así que el catálogo contesta por la relación que se lee. - Y por encima del filtro de cada motor, una clave cuyas columnas no están entre las que se midieron se descarta entera —en cualquier motor, incluidos los que no se probaron— con un motivo que lo explica. No se recortan las columnas ajenas para publicar el resto: eso daría una clave que ningún catálogo declara.
La mediana consolidada se activa donde el motor la acepta
- Las sondas que habilitan la mediana en una sola consulta ordenaban por una constante. SQL Server rechaza constantes en el
ORDER BYde una función de ventana, así que su camino nunca se activaba; MariaDB implementaPERCENTILE_CONTsólo como función de ventana y estaba clasificada con la forma sinOVER. Las dos quedaban degradadas a una consulta por columna sin aviso, porque los valores publicados eran correctos. - DuckDB acepta la forma consolidada y no se le ofrecía. Con esto, cuatro de los siete motores medidos resuelven la mediana en una sola consulta en vez de una por columna.
README, API y recorrido guiado
- Los dos README ahora empiezan por el problema que resuelve
lupa, los tipos de hallazgo que permiten ver más que unsummary(), la cobertura medida de AGESIC, CEPAL e ISO 25012, los siete motores comprobados contra un motor real y el recorrido mínimo para empezar. La documentación enumera los 57 nombres canónicos detipo_hallazgo. - Los argumentos de
medir(),evaluar()ycobertura_analisis()indican en qué posición se recibe cada objeto y distinguen el perfil descriptivo deperfilar()del perfil de evaluación deperfil_evaluacion(). Los errores de tipo equivocado ahora nombran el argumento y el constructor esperado. - El catálogo de motores ya no presenta Oracle como medido: las dos variantes quedan documentadas como dialectos esperados sin comprobación contra un motor real, en línea con la cobertura publicada.
- Se añade la viñeta
flujo-guiado, que recorre una propuesta editable desdeperfilar()hastaevaluar()y muestra el orden de la API.
La clave declarada separa ausencias y repeticiones
-
perfilar(clave = ...)publicaclave_con_ausentespara las filas que impiden garantizarNOT NULL, y reservaclave_no_unicapara valores repetidos entre filas con la clave completa. La descripción, la evidencia, los conteos y las trazas comparten ahora el universo demeta$clave$unicidad, por lo que una colisión entre ausentes no se presenta como una refutación de la unicidad. - La documentación de
perfilar()y ambos README explican la separación y los conteos de cada hallazgo.
La trazabilidad de filas duplicadas conserva todos los participantes
- La traza de
filas_duplicadasusa la misma comparación en ambas direcciones que el conteo. Esto corrige las columnasinteger64, cuyos métodos pueden ignorarfromLast, y mantiene alineadosn_afectadosytrazabilidad$total.
El minimo de R conserva la conducta en BLOB y duplicados matriciales
- Se conserva
R (>= 4.1.0). En el conjunto minimo reproducido, un BLOB de SQLite puede llegar comoblob/vctrs_list_ofde valoresraw, cuyounique()no funciona con versiones antiguas devctrs. La columna ya no aborta el perfil:is.na()produce el hallazgo de faltantes y su trazabilidad, mientras la escala y los agregados cuantitativos quedan como desconocidos ono_aplica. - Las versiones de R hasta 4.1 y desde 4.2 no tienen la misma semantica en
base::duplicated.data.frame()para columnas matriz: la primera compara sus celdas y la segunda las separa por filas.lupaya normalizaba esas columnas antes de contar; el test fija ahora esa conducta estable en1fila duplicada y2filas participantes, sin usar como esperado una semantica de base que cambia con R.
El plan DBI declara el muestreo que no puede construir
-
plan_perfilado_dbi()comprueba la forma SQL del muestreo sin leer datos, declara el estado enattr(plan, "muestreo")y excluye del rango las métricas que no pueden emitirse. La corrida comparte esa decisión y conserva cada ausencia con su motivo. - La documentación corrige el resumen de
perfil_muestraa 109 campos analíticos además del nombre de la columna, y describe quemuestratambién limita la búsqueda de dependencias. El valor por omisiónInfya no se presenta como una elección distinta deInf; también se elimina la referencia a unmax_paresque no es argumento deperfilar().
El plan declara la memoria del procesamiento como no estimada
-
plan_perfilado_dbi()ahora imprime un bloque que declara la ausencia de una estimación honesta de la memoria del procesamiento, conserva la magnitud del trabajo (filas, celdas y pares de texto) y separa esos datos de referencias medidas de otras corridas. - La documentación deja explícito que ver todas las filas no equivale a tenerlas todas en memoria: el costo observado está en procesar en R, no en traer los datos ni en el motor. El plan no emite consultas adicionales.
La regla de vocabulario también mide el largo que compara
- El tope de largo se había corregido en
detectar_duplicados_aproximados(), que pasó a medir sobre la cadena combinada y normalizada. La regla hermana —la proximidad de vocabulario dentro deperfilar()— quedó midiendo el valor guardado mientras comparaba el normalizado. Con la normalizaciónamplio, que expande ligaduras, una columna de 5.101 caracteres publicabalargo_excedido = FALSEy comparaba cadenas de 15.303: un 53 % por encima del tope declarado. - El desvío tiene dos direcciones y las dos están corregidas: la normalización también acorta —colapsar espacios lleva 12.499 caracteres guardados a 7.501 comparados—, y ahí la regla excluía una columna que se podía comparar sin problema.
-
alcance$largo_maximopublica ahora el mismo número en las dos ramas: el largo comparado. Antes, la rama que excluía informaba el comparado y la que no excluía informaba el guardado, así que dos informes no eran comparables entre sí.
La clave primaria contempla los dos esquemas que resuelve SQL Server
- Un nombre sin calificar se resolvía filtrando por
SCHEMA_NAME(), que es sólo el primer paso: SQL Server busca el esquema por omisión del usuario y, si no está ahí,dbo. Una tabla declarada sólo endbo, consultada por un usuario con otro esquema por omisión, se lee del motor pero el catálogo devolvía cero filas y se publicabano_declarada, que era falso. - Ahora se piden los dos y decide la red de seguridad: si la tabla existe en uno solo, se publica su clave; si existe en los dos, no se publica ninguna y el motivo lo explica. Se pierde una respuesta en un caso raro y ninguna es falsa.
Los avisos DBI de costo tienen controles independientes
-
perfilar_dbi()agregaavisar_costo_distintosconumbral_segundos_aviso_distintos, yavisar_derrame_estimadoconumbral_bytes_aviso_derrame_estimado: el costo deCOUNT(DISTINCT)se controla en segundos y el derrame estimado en bytes.Infdesactiva explícitamente el aviso; el umbral histórico de 30 segundos se conserva y el derrame mantiene el comportamiento de avisar cualquier lote estimado por encima de la memoria efectiva. - Los controles sólo silencian el cartel:
meta$costo_distintos,meta$derrameymeta$estimacion_derramesiguen publicándose. Los avisos DBI continúan sonando en guiones porque el costo se paga en el servidor; la diferencia con el aviso interactivo de tabla ancha queda documentada enperfilar_dbi().
La clave primaria se lee de la tabla que se midió, y de ninguna otra
- Un nombre de tabla sin calificar traía del catálogo la clave de todas las tablas homónimas, de todos los esquemas, y las fusionaba en una sola respuesta. Con
public.dup(claveid),s1.dup(a,b) ys2.dup(x),perfilar_dbi(con, "dup")publicabacolumnas = id, a, x, bcon garantíagarantizada: una clave que no existe en ninguna tabla. Reproducido también contra MySQL, donde el mismo agujero cruza bases de datos. - Ahora cada vía resuelve el nombre como lo resuelve el motor:
pg_table_is_visible()en PostgreSQL —que respeta elsearch_pathy devuelve la misma relación de la que se leen los datos—,DATABASE()en MySQL ySCHEMA_NAME()en SQL Server. Compatible con PostgreSQL 9.3. - Y por encima hay una red que no depende del motor: si las filas del catálogo pertenecen a más de una relación, no se publica ninguna clave y el motivo dice cuántas eran, en qué esquemas, y que calificar el nombre lo resuelve. Fusionar nunca es una opción.
- El mismo cambio corrige una tabla temporal con clave declarada, que se publicaba como
no_declarada: «no se pudo preguntar» disfrazado de «no declara».
Una garantía desconocida dice por qué no se pudo saber
- Sobre MariaDB, la garantía de la clave salía
desconocidaconmotivovacío. El diagnóstico era correcto —MariaDB no exponeENFORCEDeninformation_schema.TABLE_CONSTRAINTS, a diferencia de MySQL—, pero quien perfilaba no podía distinguir entre un privilegio que le falta, un motor no cubierto y un límite del catálogo. El motivo ahora nombra la vía, el motor y los campos que no se pudieron consultar, y descarta las dos lecturas equivocadas.
El tope de largo mide la cadena que se compara
- El tope se evaluaba sobre el valor guardado y la comparación corría sobre el combinado y normalizado. Dos columnas de 9.000 caracteres pasaban el tope de 10.000 y se comparaban unidas en 18.003; y con la normalización
amplio, que expande ligaduras, un valor de 5.101 caracteres se comparaba expandido a 15.303. En los dos casosalcance$largo_maximopublicaba el largo guardado, que no era el que se había medido. - Ahora el tope se mide sobre la cadena que entra a la comparación y
largo_maximopublica ese largo. Sin normalización que expanda y con una sola columna, la conducta no cambia. - Cuando el tope no aplica —
Inf— no se mide ningún largo ylargo_maximovaleNAen vez de0: un cero ahí afirmaba una medición que no ocurrió.
Los valores largos se cuentan en valores, y las filas en filas
- La cobertura del vocabulario informaba «100 valores superan el tope» cuando había un valor distinto repetido en 100 filas. La regla de proximidad trabaja sobre el vocabulario, así que ésa era la unidad prometida y no la medida. El motivo publica ahora las dos cuentas, cada una en su unidad.
El derrame de COUNT(DISTINCT) se avisa antes de pagarlo
- En PostgreSQL,
perfilar_dbi()consultapg_stats.n_distinct,pg_stats.avg_width,pg_class.reltuplesywork_mempara estimar el tamaño del hash de agregación antes del primerCOUNT(DISTINCT); en PostgreSQL 13 o posterior incorporahash_mem_multiplieral límite efectivo. - El aviso dice el tamaño estimado, la memoria vigente y que subir
work_memen la sesión puede evitar el derrame. La configuración nunca se modifica. El diagnóstico queda separado demeta$derrame, rotulado siempre como estimación; si luegopg_stat_statementsmide un derrame, esa evidencia posterior prevalece. - Permisos insuficientes, tablas sin
ANALYZE,reltuplesdesconocido, particiones sin estadísticas, PostgreSQL 9.3 y motores que no son PostgreSQL quedan declarados como no estimables, sin presentarlos como ausencia de derrame. Se agregan pruebas unitarias e integración contra PostgreSQL 16 y 9.3.
Topes declarados para valores largos y tablas anchas
-
detectar_duplicados_aproximados()y el vocabulario deperfilar()tienen por defecto un tope de 10.000 caracteres por valor. La distancia normalizada se midio con pares que diferian en un caracter y en 1.000: el segundo par dejo de separarse del umbral 0,10 entre 25.000 y 50.000 caracteres, mientras el primero siguio cerca de cero. El tope se declara enalcanceo encobertura_diagnosticos;Infrecupera explicitamente el comportamiento anterior. - Una columna con valores por encima del tope queda fuera de la comparacion completa y la cobertura publica la columna, el largo observado, el motivo y el umbral. No se recortan valores en silencio.
-
perfilar()conserva enmeta$costo_tabla_anchauna proyeccion del costo por celdas y avisa en sesiones interactivas desde 100.000 celdas. El mensaje identifica la fuente y aclara que es una estimacion; no aparece en scripts ni por debajo del umbral.avisar_costo_tabla_ancha = FALSEyumbral_celdas_aviso_tabla_ancha = Infson desactivaciones explicitas. - Se agrego
benchmark/medir_topes_valores_ancho.Rpara reproducir la medicion de discriminacion y el barrido de tablas anchas.
El parseo de fechas también trabaja sobre el vocabulario
Después de la detección de formatos, el parseo era lo que quedaba recorriendo todos los valores. Ahora deduplica, parsea las formas distintas y mapea de vuelta, que es el mismo camino.
Sobre una columna de fechas realista —1.096 fechas distintas en tres años, 120.000 filas—, perfilar() baja de 4,23 s a 3,24 s: un 23 % menos. El costo del parseo queda prácticamente plano respecto de la cantidad de filas.
El vector devuelto es idéntico: 257 comparaciones aleatorias contra la implementación anterior, con comparación exacta y no de un resumen, incluyendo meses en texto, zonas horarias, granularidad de mes, formatos mezclados, valores no parseables, columnas ya Date y ya POSIXct.
La clave primaria queda publicada en el perfil DBI
perfilar_dbi() consulta siempre el catalogo de la clave primaria y conserva la respuesta en resumen_tabla$meta$clave, junto a sus columnas, fuente, motivo, garantia y estado. Una tabla sin clave declarada queda con garantia = "no_declarada"; una consulta fallida o una visibilidad que no se pudo establecer queda con garantia = "desconocida" y su motivo. Se conservan sin cambios los estados ya definidos para garantia, visibilidad, restricciones diferibles, descendientes e indice de respaldo.
La lectura agrega una sola consulta de catalogo y ninguna consulta de datos. El metodo de impresion muestra la diferencia entre una clave garantizada, una tabla sin clave declarada y un catalogo que no se pudo consultar. El plan sigue sin recorrer datos: la consulta adicional es de metadatos.
La moda conserva su guardian de la misma sentencia
- La consulta de la moda intenta traer
SUM(COUNT(*)) OVER () AS n_validos_guardjunto a la frecuencia y validafrecuencia_moda <= n_validosdentro de esa misma sentencia. - La forma se sondea antes de usarla. Si el motor la rechaza, se conserva la consulta anterior y
resumen_tabla$meta$moda_guardiandeclara el repliegue; el motivo deja de afirmar que la cota no se pudo comprobar cuando el guardian estuvo disponible. - En
EXPLAIN (ANALYZE, BUFFERS)sobre una tabla temporal de 2 millones de filas, PostgreSQL 9.3 pasó de una mediana de 245,7 ms a 271,7 ms y PostgreSQL 16 de 258,5 ms a 277,6 ms. Ambos planes conservaron un soloSeq Scany los mismos buffers; el costo agregado fue la ventana sobre los tres grupos ya calculados.
El detector de formatos de fecha trabaja sobre el vocabulario
Una columna de fechas suele tener pocas formas distintas repetidas muchas veces, y el detector las evaluaba todas. Ahora clasifica las formas del vocabulario y pondera por su frecuencia, que es el mismo camino que ya usaba la clasificación de tipos.
Los conteos no cambian: el detector devuelve los mismos formatos, las mismas frecuencias, las mismas ambigüedades y los mismos atributos. Trabajar por vocabulario y ponderar no altera un solo número, y hay pruebas de equivalencia campo por campo y atributo por atributo que lo fijan.
Medido sobre 120.000 filas y cinco columnas, el detector pasa de 1,16 s a 0,050 s, y perfilar() completo baja de 5,30 s a 3,93 s: un 26 % menos. La ganancia crece con las filas, porque el trabajo pasa a ser proporcional a los valores distintos y no a la cantidad de filas.
La mediana conserva una sola instantanea
- La mediana exacta de los dialectos que usan
LIMITreemplaza el conteo y el recorte en dos consultas por una sola sentencia con subconsultas escalares. La sonda verifica la forma y la division entera de%y/; en SQLite y PostgreSQL 9.3 se conserva el camino conLIMIT. - Si el dialecto no acepta esa forma, se conserva la via de dos consultas y el metodo queda declarado en
resumen_tabla$sql. La ruta dePERCENTILE_CONT(...) WITHIN GROUPno cambia. - La forma nueva coincide con la via anterior en nueve bordes de la mediana, incluidos nulos, tabla vacia, repetidos y negativos; se verifico en SQLite y contra PostgreSQL 9.3.
La proyección temporal de distintos usa su propia unidad
-
perfilar_dbi()mide el primer lote deCOUNT(DISTINCT)y, si queda otro, anuncia después de ese lote y antes del siguiente una proyección basada en la mediana de sus consultas, multiplicada por la cantidad total de lotes. La estimación publica el costo, la fuente medida y la cantidad de lotes; no usareltuples, no cambiawork_memy no espera confirmación en guiones no interactivos. Con un solo lote no publica una proyección. La misma regla funciona cuando se pide sólometricas = "distintos". -
plan_perfilado_dbi()conserva su promesa de no emitir consultas de datos y declara que no proyecta el costo temporal antes de correr: sólo una ejecución puede medir la referencia honesta, en la unidad de los distintos. - La instrumentación de PostgreSQL consulta
pg_stat_statementsantes y después de los distintos exactos. Sólo publica derrame real cuando puede atribuir exactamente una llamada:resumen_tabla$sqlagrega los bloques temporales ymeta$derrameconserva el estado, la fuente y el motivo. La falta de evidencia quedano_disponible, no se infiere del reloj. -
tamano_lote_distintosconserva el valor medido 2; el cambio no modificawork_mem.
resumen_tabla$sql declara su unidad sumable
-
resumen_tabla$sqlconserva una fila por columna y métrica, peroduracion_mspertenece a la consulta y puede repetirse en varias filas. La nueva columnanivelsigue la semántica deresumen_tabla$tiempos:nivel = 1identifica la primera fila de cadaconsulta_idynivel = 2las filas repetidas. - La suma segura de
duracion_msusa sólonivel = 1; las filas sin consulta siguen teniendoNA. Así la tabla hace visible la unidad que evita contar una consulta más de una vez.
La política de costo no ciega las fuentes estructurales
-
politica_costo = "por_cardinalidad"resuelve las fuentes estructurales de cardinalidad aunqueestrategia_distintosesté omitida o no disponible. La disponibilidad gobierna la medición, no una garantía de clave que el catálogo ya permite saber. No hay repliegue aCOUNT(DISTINCT ...)para una estrategia omitida, de catálogo o aproximada sin capacidad. -
resumen_tabla$meta$snapshotdeclara que no hubo lectura instantánea. Cuando valores exactos incoherentes den_validosyn_distintosvienen de grupos de consistencia distintos,coberturasumaalcance_distintoy conserva en el motivo las dos sentencias: es evidencia de que la tabla cambió durante la corrida, no una acusación contra el motor o el paquete.
Cada agregado queda ligado a la foto que lo produjo
Cada lote de agregados planos que calcula n_validos trae también COUNT(*) AS n_total_consulta en la misma sentencia. La completitud usa ese denominador local, incluso cuando la bisección separa un lote; no combina el total de una consulta con válidos de otra. El total del universo se conserva aparte sólo cuando el perfil sobre una muestra lo necesita o cuando no hay un agregado que pueda llevarlo.
La consulta exacta de distintos trae COUNT(columna) AS n_validos_guard junto a COUNT(DISTINCT columna). consulta_id, ya publicado en resumen_tabla$sql, identifica la sentencia y el grupo de consistencia: las cotas duras sólo se aplican dentro de ese grupo. Cuando una capacidad aproximada no puede traer su guardián, la comprobación queda declarada como no disponible y el motivo no culpa al motor.
Se agregaron regresiones con INSERT y DELETE entre consultas, además de rastreo de SQL para comprobar que el denominador viaja en la consulta que ya se emitía y no agrega una ida y vuelta.
La garantía de una clave mira el índice que la impone
pg_constraint dice que hay una restricción; el que impone la unicidad es el índice que la respalda. La lectura del catálogo trae ahora indisunique de ese índice, con un LEFT JOIN en la consulta que ya se emitía, y si no es único la garantía baja aunque la restricción figure validada.
No es un estado alcanzable por DDL normal —al adjuntar una partición el motor crea solo el índice único y válido—, así que es defensa ante un catálogo alterado. Entra igual porque cuesta una columna y porque cambia la garantía de «el catálogo declara una restricción» a «el índice que la impone es único».
Ronda 154: el muestreo publica lo que se obtuvo y distingue su incertidumbre
resumen_tabla$meta$muestreo ahora publica juntas las filas solicitadas y las filas obtenidas por la lectura de perfil_muestra. tamano_muestra se conserva por compatibilidad como el tamaño efectivo solicitado a la consulta; filas_solicitadas identifica el pedido original y filas_obtenidas el hecho observado. Si la lectura no se hizo o falló antes de devolver filas, el último campo queda en NA.
En las métricas SQL muestreadas, error_esperado deja de ser uniforme: no_estimado indica que el error podría calcularse bajo un plan probabilístico pero no se calculó; no_estimable cubre la moda, la mediana y la cardinalidad observada, sin una cota simple o un estimador declarado; y no_aplica indica que no hubo muestreo efectivo. El motivo se conserva en cada registro junto con el método, el tamaño y la fracción. No se agregan cotas numéricas inventadas.
Cambios en desarrollo
-
perfilar_dbi()yplan_perfilado_dbi()aceptan la estrategia explícita de distintosexacta,aproximada_motor,catalogouomitida. La estrategia exacta es el valor por omisión; una aproximación o estadística de catálogo ausente quedano_disponiblesin repliegue aCOUNT(DISTINCT), y el resultado distingue lo solicitado de lo resuelto. -
plan_perfilado_dbi()deja de ejecutar agregados de datos para decidir el costo. Con una cardinalidad desconocida publica un rango y conserva separadasestrategia_distintosyfuente_cardinalidad_costo. -
perfilar_dbi()ejecuta primero los agregados planos, luego el total exacto, los distintos, la moda y la mediana.tamano_lote_planosytamano_lote_distintosson independientes; este último es 2 por omisión, valor medido en el servidor de referencia. La mención anterior a 1 queda corregida aquí.
Una clave heredada ya no se declara garantizada sobre otro universo
En PostgreSQL, una consulta sin ONLY incluye a las tablas que heredan, y la clave primaria del padre no gobierna las filas de los hijos. El catálogo sigue informando la restricción como válida, así que perfilar_dbi() publicaba garantia = "garantizada" sobre un universo donde la unicidad puede no cumplirse: medido, una tabla con un hijo que repite un valor da 6001 valores y 6000 distintos en la consulta que ejecuta el paquete.
Ahora la misma consulta de catálogo trae si la tabla tiene descendientes —sin agregar una ida y vuelta— y en ese caso la garantía baja a declarada_no_garantizada, con el hecho anotado en estado$universo_incluye_descendientes. La restricción existe y es válida; lo que no vale es sobre las filas que se van a perfilar.
El particionado declarativo no pierde la garantía. pg_inherits registra tanto la herencia tradicional como las particiones, y sólo la primera deja filas fuera del alcance de la clave: el motor exige que la clave de una tabla particionada incluya sus columnas de partición, justamente para poder garantizarla sobre el árbol. Medido: una tabla regular con un hijo que repite un valor da 6001 válidos y 6000 distintos; una particionada con dos particiones da 19999 y 19999. Se distinguen por relkind, en la misma consulta. En PostgreSQL anterior a la versión 10 no existe el particionado declarativo, y ahí toda descendencia es herencia.
La misma consulta trae también si la restricción es DEFERRABLE. Una clave diferible puede estar violada mientras una transacción sigue abierta, y el catálogo la informa validada igual: medido, dentro de una transacción que inserta un duplicado la tabla tiene 3 valores válidos y 2 distintos. Desde el paquete no se puede saber si hay una transacción con violaciones pendientes, así que la garantía no se afirma y queda estado$restriccion_diferible.
El estado publicado queda atado a la consulta ejecutada
Los conteos aproximados sólo se consolidan cuando el adaptador entrega una expresión que se puede incrustar en el SELECT. Una capacidad que sólo construye consultas completas conserva los conteos de válidos y distintos por separado, sin presentar COUNT(DISTINCT ...) como una aproximación. Un resultado no emitido no se publica como estimado, y el registro fuerza no_disponible cuando ok es falso.
La cota n_distintos <= n_validos se valida antes del único registro. Los valores imposibles no se recortan ni se publican; el valor bruto queda en el motivo de auditoría.
Catalogos de claves sin confundir ausencia con falta de visibilidad
La lectura DBI ya no llama no_declarada a cualquier consulta de catalogo que vuelve vacia. Una consulta fallida queda con garantia = "desconocida", visible = NA y su motivo; una vista que devolvio cero filas pero puede ocultar metadatos queda con garantia = "desconocida", visible = NA y la ambiguedad escrita en motivo. Un error de permisos identificable queda con visible = FALSE; solo las vias cuyo catalogo es visible para una tabla accesible conservan no_declarada.
Medido contra MariaDB 11 y MySQL 8 reales, con una cuenta propietaria y otra con solo SELECT, SHOW INDEX devolvio las filas PRIMARY en las dos cuentas para las claves simples y compuestas, y cero filas en las tablas sin clave. MariaDB usa esa via en una sola consulta y conserva el orden de las columnas con Seq_in_index; su garantia sigue desconocida porque esa salida no publica un estado comparable de aplicacion y validacion.
PostgreSQL deja de partir de information_schema.table_constraints, que oculta restricciones a un rol que solo tiene SELECT, y consulta directamente pg_constraint, pg_class, pg_namespace y pg_attribute, conservando el orden de una clave compuesta y el estado convalidated. La ruta contra un servidor PostgreSQL con una credencial que solo tiene SELECT queda pendiente de verificacion en esta entrega.
SQLite pide tambien notnull. Su estado separa unicidad = "garantizada" entre los valores no nulos de la PRIMARY KEY de ausencia_de_nulos: es "garantizada" cuando todas sus columnas devuelven notnull = 1 y "no_verificada" en los demas casos. La garantia conjunta queda desconocida cuando la no-nulidad no se puede sostener. Esto subclasifica honestamente los casos especiales que pueden garantizar no-nulidad sin que este camino distinga su declaracion, como INTEGER PRIMARY KEY; no se lanza un recorrido de los datos. Se verifico con dos NULL reales en una PRIMARY KEY de texto sin NOT NULL, y con el rechazo de un NULL en otra con NOT NULL.
La selección de columnas pasa por una primitiva con semántica declarada
El paquete deja de escribir datos[, columnas, drop = FALSE] en cada sitio que recibe una tabla y lo hace a través de una función interna cuya semántica de referencia está declarada, de modo que el significado no dependa de la clase de la tabla ni del estado del espacio de nombres. Esto corrige además selecciones de una dimensión que quedaban en agregacion, claves-relaciones, duplicados-aproximados, referencial, remediacion y tablero-calidad: tabla["columna"] selecciona una columna en un data.frame y en un tibble, pero en un data.table intenta un cruce y aborta. No agrega conversiones: sigue habiendo una sola por llamada.
SQL Server lee su catálogo por la vista estándar
La clasificación de visibilidad de information_schema.table_constraints deja de tratar a SQL Server como ambiguo. Medido con un rol de sólo SELECT sobre tablas con clave simple, compuesta y sin clave, la vista devuelve las restricciones: la vía es exhaustiva para esa credencial, así que un resultado vacío significa que la clave no está declarada. Lo sostienen dos mediciones independientes sobre dos versiones distintas del motor.
La unicidad de una clave se evalúa entre las filas con la clave completa
perfilar(clave = ...) evaluaba la unicidad sobre todas las filas con la semántica de R, donde dos ausentes de la misma posición colisionan. Eso hacía que una clave cuyas únicas repeticiones venían de filas incompletas se informara como «no es única», cuando en SQL dos NULL no son iguales y no violarían nada.
Ahora la unicidad se evalúa sólo entre las filas con la clave completa, y la colisión entre ausentes se informa donde corresponde: en trazabilidad, porque lo que deja ambiguo es el localizador. unicidad$semantica pasa de "R" a "claves_completas", y se agrega unicidad$filas_totales junto a filas_evaluadas, que ahora cuenta las filas evaluables.
Cuando ninguna fila tiene la clave completa, el estado es "sin_casos_evaluables". Decir "verificada" sería cierto sobre un conjunto vacío y engañoso a la vez, y "no_verificada" sería falso si se recorrió la tabla entera y se comprobó que no había casos.
El conteo de duplicados no depende de ajustes globales de la sesión
data.table::setNumericRounding() es un ajuste global que cambia cuántos bits se comparan de un doble al ordenar. Como la vía rápida ordena, con los valores 1 y 2 agrupaba valores que duplicated() distingue: medido sobre dobles separados por un eps, sobre POSIXct con diferencias de microsegundos y sobre magnitudes grandes, los tres divergían. La vía rápida ahora sólo corre con precisión completa, y sólo consulta el ajuste si hay columnas dobles. lupa no lo modifica: cambiarlo alteraría el comportamiento de código ajeno.
Se comprobó además que el número de hilos, la configuración regional, la zona horaria, la codificación y las opciones de data.table no cambian el resultado.
Un duplicated() que ignora fromLast ya no cambia el conteo
bit64 devuelve para una columna integer64 el mismo vector con fromLast = TRUE que sin él. La cuenta de filas en grupos repetidos heredaba ese defecto: una tabla de 30 filas con 3 valores distintos daba 27 en vez de 30, y la respuesta cambiaba según el umbral de filas, porque la vía rápida sí daba 30. El conteo hacia atrás se calcula ahora dando vuelta las filas, que no depende de que el método respete el argumento.
La clase de la tabla ya no cambia el resultado
perfilar(), analizar() y el resto de la API que recibe tablas dan el mismo resultado con un data.frame, un data.table o un tibble. Las entradas se normalizan en la frontera —una sola vez por llamada— y las tablas internas del modelo de calidad también, de modo que la sintaxis de selección de columnas significa lo mismo en todos los caminos.
Esto corrige además cinco selecciones de una dimensión que no eran portables: tabla["columna"] selecciona una columna en un data.frame y en un tibble, pero en un data.table intenta un cruce y aborta. Ahora son selecciones explícitas de dos dimensiones.
Conteo exacto de filas duplicadas sin mutar la entrada
El contador de filas duplicadas de perfilar() obtiene los grupos de filas con data.table::frank() en vez de armar la estructura intermedia que combina todas las columnas, que es lo que hace duplicated() sobre un data.frame y lo que hace crecer su costo con el ancho de la tabla. La entrada no se convierte ni se copia.
data.table se llama con :: y no se importa al espacio de nombres, a propósito: data.table decide la semántica de [ según si el paquete que llama lo tiene entre sus imports, de modo que importarlo cambiaría el significado de tabla[, columnas, drop = FALSE] en toda función que recibe una tabla de quien usa el paquete. frank() no depende de esa condición; duplicated() sobre un data.table sí, y por eso no se usa.
Las columnas de lista o matriz se detectan antes y usan la semántica de base, y también las tablas con NaN: frank() ordena y no distingue NaN de NA, mientras que duplicated() sobre un data.frame sí los distingue. La dependencia pasa de Suggests a Imports, y el resultado queda cubierto por pruebas de equivalencia contra base sobre data.frame, data.table y tibble.
Claves declaradas: unicidad, ausencias y trazabilidad separadas
perfilar(clave = ...) informa por separado si la combinación es única bajo la semántica de R y si sus componentes están completos. Cada eje queda como verificada, refutada o no_verificada; una ausencia no se presenta como un duplicado SQL. Si la semántica de R agrupa dos ausentes para localizar filas, esa colisión y la falta de valores se informan juntas y quedan en meta$clave, junto a la trazabilidad. Una clave única y completa conserva el objeto histórico sin metadatos adicionales.
Garantía de claves primarias según el estado del catálogo
La lectura DBI de claves primarias separa la fuente (fuente) de la garantía (garantia) y conserva el estado consultado. Oracle lee STATUS y VALIDATED: una restricción deshabilitada o no validada queda declarada en el catálogo, pero no garantizada; si esos estados no se pueden consultar, quedan desconocidos. PostgreSQL consulta enforced y su estado de validación, y MySQL enforced; MariaDB, SQL Server, SQLite y DuckDB no ofrecen en esta vía un estado comparable, por lo que el resultado no inventa una garantía. Los casos Oracle se verifican con respuestas DBI simuladas; no se afirma una prueba contra un servidor Oracle real.
CPU del cliente y política explícita para métricas caras
La instrumentación de DBI publica ahora cpu_ms junto a duracion_ms en resumen_tabla$sql y en resumen_tabla$tiempos. Se calcula con proc.time() como user.self + sys.self: cerca de cero distingue espera del trabajo del proceso cliente, y cerca de uno al dividir CPU por tiempo transcurrido indica que el cliente está trabajando. Cero es una medición válida; NA queda reservado para lo que no se pudo medir, y instrumentar = FALSE deja el campo en NA como los demás. En un microbenchmark de un millón de consultas simuladas, dos lecturas de proc.time() costaron 1,122 microsegundos por consulta; dos de Sys.time() costaron 1,778 microsegundos. En la tabla ancha de 158 columnas, el agregado no duplicó el costo de instrumentar: cinco corridas con reloj solamente tuvieron mediana de 1,093 s y cinco con reloj más CPU de 0,962 s, una diferencia dominada por la variación de SQLite. El número reproducible que se publica es el costo directo de 1,122 microsegundos por consulta.
Moda y mediana se pueden controlar con politica_costo. El valor por omisión es "todas", que conserva el perfil anterior; "por_cardinalidad" hace primero los conteos baratos de valores válidos y distintos y decide luego por columna. Si n_distintos / n_validos alcanza umbral_cardinalidad, omite la moda de esa columna. La omisión no desaparece ni se vuelve NA silencioso: queda omitido_por_costo con el motivo, la proporción observada y la forma de pedirla igual (politica_costo = "todas") o mover el umbral.
Cuando se escribió esta entrada el umbral valía
0.95y omitía moda y mediana. Las dos cosas cambiaron al medirlas —el umbral es0.5y la mediana ya no se omite por proporción, ver la entrada de arriba—, y el texto se corrigió acá para que no queden dos descripciones del mismo argumento diciendo cosas distintas en el mismo archivo.
La política hace explícito el plan en dos etapas. En una tabla reproducible de 158 columnas, 80 numéricas, 200 filas y 60 columnas con cardinalidad al menos 0,95, politica_costo = "todas" emitió 260 consultas y la política selectiva 140: se ahorraron 120 consultas, no tiempo. Se omitieron 60 modas y 60 medianas. El valor por omisión no cambia: esos 60 casos muestran el ahorro posible, pero no autorizan al paquete a elegir qué métrica sacrificar.
Cuando la sonda reconoce un motor con PERCENTILE_CONT(...) WITHIN GROUP, las medianas numéricas se consolidan en un SELECT por lote; el camino actual, una mediana por columna, queda como respaldo si la capacidad no está disponible o falla la consulta consolidada.
El recorrido que se pagaba sólo por contar, y qué filas vio cada métrica
El COUNT(*) exacto ya viaja en la primera consulta de agregados. Si el lote completo es rechazado, lupa emite un COUNT(*) solo como repliegue y continúa con la bisección: la completitud sigue derivando n_faltantes y prop_faltantes de un denominador medido, nunca estimado. El plan sigue pagando su propio conteo porque necesita conocer el total antes de estimar el trabajo.
En una tabla en memoria de 12 filas y tres columnas, con tamano_lote = 4 y bloque_muestra = "solo_agregados", la traza SQL dio estos conteos. La columna de recorridos cuenta las apariciones de la fuente en el SQL; no se usó tiempo.
| modo | consultas antes | consultas ahora | recorridos antes | recorridos ahora | ahorro de recorridos |
|---|---|---|---|---|---|
exacto |
14 | 13 | 14 | 13 | 1 |
seguro |
8 | 7 | 8 | 7 | 1 |
conteos |
6 | 5 | 6 | 5 | 1 |
muestreado |
23 | 22 | 23 | 23 | 0 |
aproximado |
23 | 22 | 23 | 22 | 1 |
En resumen_tabla$sql, id_consulta identifica la consulta de datos: el mismo identificador garantiza exactamente las mismas filas. Moda, frecuencia de la moda y mediana son métricas por columna y quedan con NA; también queda NA cualquier camino que no pueda sostener esa garantía. Así la comparabilidad se comprueba por comparación directa, sin cruzar lote ni columnas_compartidas.
Aislar la columna culpable en pocas consultas, y no recorrer tres veces lo que cabe en una
Los lotes de agregados que el motor rechaza ya no se reintentan columna por columna sin información. La vía de agregados reutiliza la bisección de la lectura de muestras: sondea mitades, conserva los grupos aceptados como datos medidos y reintenta por métrica sólo las columnas culpables. El tope sigue siendo de hasta 2n - 1 sondas para un lote de n columnas y, si el presupuesto se agota, las hojas pendientes quedan no_disponible sin ser supuestas. El tamaño mayor de lote aceptado se guarda sólo en el presupuesto de la corrida y se publica como meta$tamano_lote_funciono; no queda estado global asociado a la conexión.
Los agregados planos sobre la misma tabla y filtro —COUNT(col), mínimos, máximos, medias, ceros, negativos y desvío— comparten ahora una consulta por lote. COUNT(DISTINCT ...) conserva su clase separada. La fusión no cambia las métricas ni su disponibilidad: una falla de un agregado en una columna todavía se prueba por separado antes de declararla no disponible.
La medición se hizo sobre una tabla en memoria de 20 columnas y 100 filas, recreando la expresión en cada vuelta, con muestra = 12 y bloque_muestra = "solo_agregados". Las consultas emitidas antes y después fueron, en orden exacto, seguro, conteos, muestreado, aproximado:
| modo | antes | después | ahorro |
|---|---|---|---|
exacto |
50 | 49 | 1 |
seguro |
10 | 8 | 2 |
conteos |
6 | 6 | 0 |
muestreado |
59 | 58 | 1 |
aproximado |
59 | 58 | 1 |
El ahorro es deliberadamente pequeño en el modo por omisión porque COUNT(DISTINCT ...) y la moda siguen fuera de la consulta plana; la mediana también, salvo en los motores cuya sonda acepta la consolidación con PERCENTILE_CONT. La fusión paga sobre todo en seguro, donde las tres pasadas planas pasan a una. plan_perfilado_dbi() refleja esas clases y su rango de sondas por bisección.
El plan ya no cobra trabajo de R que no va a ocurrir
Con bloque_muestra = "solo_agregados" no se trae ninguna fila a R, así que el detector de vocabulario no corre. El plan lo reflejaba bien en cuatro modos y mal en "muestreado": seguía anunciando los pares de formas a comparar en R, y el texto impreso se contradecía a dos líneas de distancia —«el plan incluye sólo agregados SQL» y después «más 4.000.000 pares de formas a comparar en R»—.
El conteo colgaba de un conjunto de alcances que mete en la misma bolsa dos cosas distintas: el muestreo del motor, que en ese modo ocurre igual, y el bloque del cliente, que es lo único que trae filas. Ahora cuelga sólo del segundo.
Instrumentación de consultas y etapas R
perfilar_dbi() agrega a resumen_tabla$sql duracion_ms, cpu_ms, n_filas_resultado, bytes_resultado_r, consulta_id y etapa. También publica resumen_tabla$tiempos, con las duraciones en milisegundos de la lectura y el perfilado de la muestra, el perfilado por columna y los análisis opcionales. Las ramas sin consulta dejan NA; no se publican ceros por falta de resolución del reloj. instrumentar = FALSE apaga la medición sin cambiar el plan ni el orden o la cantidad de consultas.
Un microbenchmark de 158 columnas y 262 consultas pasó de 0,2230 s sin instrumentar a 0,2368 s con reloj, filas y bytes (+0,0138 s; 6,188 %); dos lecturas de Sys.time() solas costaron aproximadamente 10 µs por consulta. En el flujo real de perfilar_dbi() (158 columnas, 10.000 filas y 347 consultas), cinco pares alternados dieron medianas de 2,398 s y 2,419 s (+0,021 s; 0,876 %). Por eso la medición queda activa por omisión y conserva un interruptor explícito para corridas donde ese costo relativo importe.
La poda y el informe preguntan ahora por la misma función
detectar_dependencias() decide dos veces lo mismo: si un par puede alcanzar el umbral —para descartarlo sin calcularlo— y si lo alcanzó —para informarlo—. Las dos decisiones estaban escritas como desigualdades separadas, y dos expresiones algebraicamente equivalentes no son iguales en punto flotante: cada forma pierde el borde con su propio conjunto de umbrales, y ahí la poda descarta un par que el informe habría publicado.
Ahora las dos preguntan por .alcanza_umbral_dependencia(). Mientras fueran expresiones distintas, saber que coinciden exigía probar todos los umbrales; compartiendo función, no hay nada que coincidir.
No lleva tolerancia a propósito: una tolerancia cambiaría lo que el umbral significa, y eso sería parte del contrato público y no un detalle interno.
Dos podas descartaban lo que igualaba el umbral
detectar_dependencias() y detectar_relaciones() descartan pares sin calcularlos cuando el umbral es inalcanzable. Las dos comparaciones se escribían multiplicando, y la multiplicación redondea: 25 * 0.56 da 14.000000000000002, así que un par cuyo cumplimiento máximo vale exactamente 0,56 quedaba por debajo y se descartaba. Como el informe descarta sólo lo que está por debajo del umbral, ese par debía informarse.
Lo mismo en la poda por cardinalidades de detectar_relaciones(): con siete valores distintos contra veinticinco y umbral 0,28, el producto daba 7.0000000000000009 y declaraba cardinalidades_imposibles una cobertura que sí era alcanzable.
Las dos comparan ahora dividiendo. Los umbrales por omisión del paquete no disparaban el defecto —por eso no se había visto—, pero umbral y umbral_cobertura admiten cualquier proporción, y un barrido exhaustivo con la forma anterior encontró setecientas dos podas de más.
Pedir sólo agregados sin leer la muestra
perfilar_dbi() y perfilar_coleccion() aceptan bloque_muestra = "solo_agregados" para calcular los agregados SQL sin traer filas a R ni ejecutar el perfil de la muestra. El valor por omisión "con_muestra" conserva el comportamiento anterior. plan_perfilado_dbi() omite la fila y el costo de la muestra cuando corresponde, y la cobertura usa no_solicitado para distinguirlo de una muestra que se intentó leer y no estuvo disponible.
La inferencia de tipo clasifica el vocabulario repetido una sola vez
.inferir_tipo() usa el vocabulario textual cuando su cardinalidad hace conveniente el recorrido, y pondera cada forma por su frecuencia en las filas. El camino que no activa el atajo conserva las mismas pasadas sobre cada valor. La detección de fechas queda sobre las filas muestreadas: sus atributos compatibles, total, analizados, muestreado y el caché interno de meses forman parte del contrato del perfilado y no se reemplazan por conteos de formas.
Dos números que las optimizaciones habían movido
Los cambios que quitaron recorridos internos prometían no mover nada de lo informado. Movían dos cosas, las dos en el borde.
La mediana no es el cuantil 0,5 hasta el último bit. median() promedia los dos valores centrales con (a + b) / 2 y quantile(type = 7) interpola con a + 0,5 * (b - a); cuando los centrales son de magnitudes muy dispares redondean distinto. Sobre c(-1000, 0.000111, 0.25, 1000) la mediana informada pasaba de 0,12505549999999999 a 0,12505550000000001. Vuelve a salir de median(); los cuartiles siguen compartiendo una sola llamada, así que el recorrido que se ahorra sigue ahorrado.
Y la poda de dependencias callaba el par que iguala el umbral. La cota se comparaba como k_y - k_x > n * (1 - umbral), y 1 - 0.8 vale 0,19999999999999996: con cinco filas, la resta hacía que 1 > 0,99999999999999978 y el par se descartaba. Su cumplimiento era exactamente 0,8, y el filtro de informe descarta sólo lo que está por debajo del umbral, así que ese par debía informarse. Escrita como el máximo alcanzable contra lo que el umbral exige, el borde deja de perderse: medido sobre 200.000 combinaciones, la forma anterior podaba de más 88 veces y la nueva ninguna.
La deteccion de dependencias conserva el resultado y reduce el costo
La particion de parejas (determinante, dependiente) usa una clave entera cuando el producto de sus cardinalidades no supera 2^53; en el borde o fuera de el conserva el camino con interaction(). La clave solo renumera parejas, por lo que no cambia sus conteos, los grupos en conflicto ni ninguna dependencia informada.
La deteccion agrega una cota de poda basada en la cantidad P de parejas distintas del subconjunto valido: P - k_x. La particion y sus conteos se reutilizan si el par debe evaluarse, y la bateria de equivalencia compara el objeto completo con ambos atajos y con sus caminos de referencia.
Tres recorridos internos se eliminan sin cambiar lo informado
El resumen cuantitativo comparte una sola llamada a quantile() para obtener los cuartiles y la mediana, deriva el IQR de esos valores y pasa q1 y q3 al diagnóstico de sentinelas. La guarda que evita ese diagnóstico con menos de 20 valores sigue decidiendo antes de sus cuantiles. Sobre un millón de valores, la expresión equivalente pasó de una mediana de 0,048 s a 0,027 s; el resultado fue identical().
El conteo general de duplicados calcula una vez cada dirección de duplicated() y deriva los dos conteos. Los tryCatch siguen aislando un fallo inicial (NA, NA) de un fallo sólo en fromLast (valor, NA). Sobre 300.000 × 5, la mediana pasó de 0,928 s a 0,707 s; los dos conteos fueron idénticos.
La detección de dependencias poda un par sólo cuando ninguna de sus columnas tiene ausentes y la cota k_y - k_x > n * (1 - umbral) hace inalcanzable el umbral. Las cardinalidades usadas son las de estadisticas, que en ese caso son exactamente las del subconjunto válido; con ausentes el par se evalúa completo. Sobre 20.000 × 12 se podaron 36 de 132 pares y se hicieron 96 llamadas al resumen: la mediana pasó de 0,575 s a 0,444 s. El objeto completo fue identical() con la ejecución sin poda, y el caso que cumple 0,996 se conserva.
El plan declaraba cero trabajo para la tabla entera
plan_perfilado_dbi() subdeclaraba el costo justo en el caso por omisión. Con muestra = Inf —que trae la tabla entera— el bloque de muestra se contaba como cero filas leídas y cero pares de formas a comparar, de modo que pedir todas las filas declaraba menos trabajo que pedir mil. Sobre una tabla de 200.000 × 4, muestra = Inf anunciaba 400.000 lecturas y muestra = 200000 —que pide exactamente las mismas filas— anunciaba 600.000.
Como la magnitud del trabajo se decide sobre esos números, el caso por omisión caía en «baja» y el plan no imprimía las palancas para bajar el costo.
Las dos formas de pedir la tabla entera declaran ahora lo mismo. Los valores que sí son inválidos siguen tratándose como antes.
Y el hueco simétrico: una muestra finita mayor que las filas de la tabla tampoco quedaba acotada. Pedir un millón de filas de una tabla de cien no trae más de cien —la lectura real es min(n_total, muestra)—, pero el plan imputaba un millón de lecturas. La inconsistencia era interna: el trabajo del cliente sí se acotaba, así que las dos mitades de la misma cuenta usaban tamaños de muestra distintos. Ahora el tamaño efectivo se calcula una sola vez y las dos mitades lo comparten.
Las geometrías que vienen de una base ya no pierden su trazabilidad
Un hallazgo sobre una columna de geometría —una coordenada fuera del dominio, una geometría inválida, una vacía— devolvía las filas que lo respaldan sólo si la columna era un objeto sfc. Por DBI las geometrías llegan como texto WKT o como blob WKB, así que la traza salía no_disponible y el paquete avisaba de una incoherencia que no existía: los índices ya estaban calculados, y se descartaban por la clase de la columna.
Contra una tabla PostGIS real eso producía coordenada_fuera_dominio en geom: traza no disponible.
La condición pasa a ser que el análisis haya dejado sus índices, no de qué clase es la columna: son posiciones de fila y valen igual esté la geometría como sfc, como texto o como binario. Una columna común sin análisis de geometría sigue sin traza, que es lo correcto.
Faltar tablas se rechaza antes, y se nombran todas
medir() acepta los datos como un data frame o como una lista con nombre. La primera forma exigía que el modelo tuviera una sola entidad; la segunda comprobaba la forma de la lista pero no que estuvieran las tablas de todas las entidades que el modelo necesita.
El faltante aparecía después, al ligar cada métrica, con un mensaje que nombraba una sola entidad. Con tres ausentes eso son tres vueltas para el mismo problema: agregar la primera, volver a correr, chocar con la segunda.
Ahora se rechaza en la frontera, nombrando todas las que faltan y diciendo cuáles se recibieron —que es la mitad que deja ver un error de tipeo—.
No es igualdad exacta, a propósito. Una tabla que el modelo no pide se sigue ignorando: exigir que sobren cero habría apagado un uso legítimo.
El muestreo en el motor declina una muestra infinita, en un solo lugar
Desde que muestra = Inf —la tabla entera— es el valor por omisión, el muestreo en el motor no tiene sentido: quedarse con «todas» las filas no es muestrear. Eso estaba guardado rama por rama, y de tres ramas se guardaron dos. La tercera —la de TABLESAMPLE RESERVOIR (n ROWS)— pasaba Inf directo al constructor y contra un motor real producía RESERVOIR (Inf ROWS) y un error de sintaxis.
El paquete lo declaraba honestamente —«El resumen SQL se calculó y se devuelve, pero la muestra no»— pero el usuario se quedaba sin perfil de muestra sin haber pedido nada raro.
La guarda pasa a estar en la entrada de la función y no dentro de cada rama. Antes de moverla se comprobó que valiera para las tres: con Inf la fracción se satura en 1, o sea TABLESAMPLE (100 PERCENT), que es la tabla entera. Si no se hubiera saturado, la rama de porcentaje habría sido un caso legítimo y la guarda única, un error.
Guardar caso por caso es cómo se olvidó una de tres, y además deja sin proteger a la rama que se agregue después.
El aviso de trazabilidad dice de quién es el problema
Cuando un hallazgo quedaba con su trazabilidad inconsistente, el aviso decía «La trazabilidad contiene incoherencias» seguido del detalle interno. Una corrida contra bases reales lo describió como honesto pero críptico, y al mirarlo resultó peor que críptico: estaba dirigido a la persona equivocada.
Las seis condiciones que lo disparan comparan dos salidas del propio lupa —lo que el hallazgo afirma contra las filas que se le adjuntaron— y ninguna mira los datos del usuario. Es una guarda de invariante contra regresiones: la prueba que la cubre la dispara mutando n_afectados a mano después de perfilar, y comprueba que un perfil intacto no la emite.
Ahora el aviso dice, en ese orden: que es un problema de lupa y no de sus datos, que no hay nada que corregir en la tabla, qué significa para el hallazgo que está leyendo —las filas que lo respaldan pueden no corresponderse con su conteo—, que se conserva para no ocultar la evidencia, y el detalle interno con la etiqueta del hallazgo para poder reportarlo.
El alcance LSH declara que sus candidatos dependen del orden
Con MinHash/LSH el conjunto de candidatos depende del orden de las filas: el vocabulario de q-gramas se numera por orden de primera aparición y esa numeración alimenta las firmas. No es un defecto —ocurre dentro de la garantía declarada, y está medido: de los pares que cambian al barajar, ninguno supera un Jaccard de q-gramas de 0,8, donde el recall declarado es 0,9998— pero era una propiedad del resultado que vivía sólo en la documentación.
alcance gana lsh_candidatos_dependen_orden_filas y lsh_orden_vocabulario. El segundo no es decorativo: nombra el mecanismo, así que si alguna vez la numeración se canoniza, ese valor cambia y la dependencia deja de declararse sola. Ninguno de los dos aparece en el camino exhaustivo, que no tiene esa dependencia.
Una barra de progreso para las corridas largas
Ahora que la tabla entera es el valor por omisión, una corrida sobre millones de filas puede tardar minutos. Y una corrida callada no se distingue de una colgada.
En una sesión interactiva perfilar_dbi() muestra una barra que avanza contra las consultas que el plan dice que va a emitir: un total conocido, no una estimación. Y perfilar_coleccion() avanza por tabla, diciendo cuál está perfilando —que es lo que permite ver si una se trabó— y sin aparecer cuando hay dos tablas o menos. No aparece cuando la corrida es de menos de una docena de consultas —termina antes de que sirva— ni fuera de una sesión interactiva, para que la salida de un guion no traiga ruido. options(lupa.progreso = ) manda sobre eso en los dos sentidos.
No cambia ningún valor de lo que se mide, y hay una prueba que compara el perfil con la barra y sin ella para que siga siendo cierto.
La tabla entera pasa a ser el valor por omisión
perfilar_dbi(), plan_perfilado_dbi() y perfilar_coleccion() traían 1.000 filas por omisión para el perfil de muestra. Ahora traen Inf: la tabla entera.
Antes ni siquiera se podía pedir. muestra exigía una cantidad finita, así que perfilar la tabla completa obligaba a averiguar cuántas filas tenía y pasarlas a mano: se podía hacerlo, no se podía decirlo. Ahora Inf significa «toda, sea del tamaño que sea», la consulta sale sin LIMIT y tabla_completa queda en TRUE.
El cambio de omisión sigue el criterio del paquete: un análisis de calidad no se corre todos los días, y mirar menos filas para terminar antes es la concesión equivocada. Quien necesite acotar lo pide con muestra = n, y el plan lo avisa antes de empezar: «El perfil de muestra trae la tabla entera —N filas—: es el valor por omisión y sobre una tabla grande puede demorar».
Importa más de lo que parece. El resumen de tabla no se muestrea —con modo = "exacto" se agrega en el motor sobre todas las filas—, pero los diagnósticos que necesitan los valores en R —patrones, formatos, casi-duplicados— salen de esa muestra, y sin orden_muestra son las primeras filas que devuelva el motor, no una muestra aleatoria. Un defecto que viva al final de la tabla no se ve.
Medido sobre 200.000 filas con un defecto plantado al final: con el valor por omisión aparecen tres hallazgos y con Inf aparecen cinco, a cambio de 10 segundos en vez de 2. Un análisis de calidad no se corre todos los días; si el tiempo no es la restricción, Inf es la opción honesta.
El plan nombra las palancas también cuando el trabajo es medio
plan_perfilado_dbi() avisa antes de pagar, y cuando el trabajo estimado era alto listaba las opciones concretas para acotarlo. Cuando era medio, se limitaba a avisar.
Eso dejaba la decisión a medias justo donde importa. Corriendo contra motores reales, una tabla de 4,5 millones de filas en PostgreSQL tardó 6,2 minutos con las opciones por omisión, y su plan la clasificaba media: el aviso avisaba, pero quien no conociera modo = "muestreado" —que baja esa misma tabla a 39 segundos, 9,5 veces menos— no tenía cómo enterarse.
Ahora las nombra en los dos casos: modo, metricas, muestra, max_consultas, y max_trabajo_vocabulario cuando lo que pesa es la comparación de formas en R. En una tabla chica no las lista: ahí el usuario ya tiene su respuesta y cuatro viñetas serían ruido en cada corrida.
El recorte de duplicados ya no depende del orden de las filas
detectar_duplicados_aproximados() recorta con max_resultados conservando los pares más cercanos. Entre pares empatados en distancia desempataba por posición de fila, así que cuáles sobrevivían dependía del orden en que llegaron las filas y no de los datos: la misma tabla exportada con otro ORDER BY podía devolver otro subconjunto.
Medido sobre 60 grupos cuyos pares internos comparten exactamente la misma distancia, con el corte en 30: cinco órdenes distintos —natural, inverso y tres barajados— devolvían 30 grupos cada uno y no compartían ninguno. Ahora los cinco devuelven exactamente los mismos.
El desempate usa el rango canónico del valor, que es la misma decisión que ya gobernaba el recorte del vocabulario —ahí tomar las formas en orden de llegada daba 26 grupos y en orden alfabético 148—. La clave es simétrica, min y max del rango, para que tampoco dependa de cuál fila quedó primera dentro del par, y el rango se calcula una vez sobre el universo completo de la corrida: una numeración por lote sería local y haría que la comparación entre lotes dependiera del reparto.
Lo que ordenar no arregla —que un corte dentro de un empate deje afuera pares igual de cercanos— se declara. alcance gana distancia_corte, n_en_distancia_corte y corte_en_empate, y ese último no es truncado con otro nombre: da FALSE cuando el corte cae en una distancia única.
corte_en_empate se mide contra lo que el recorte descartó, no contra lo que quedó. Contar cuántos de los conservados comparten la distancia del borde no alcanza: si en el borde sobrevive uno solo, el conteo da 1 y la señal caería en FALSE aunque se hubieran tirado pares a esa misma distancia. Pasaba con cuatro filas y max_resultados = 1.
Y queda un límite que ningún orden saca: si varias filas comparten el valor comparado, el conjunto de pares de valores es idéntico en cualquier orden, pero cuáles instancias de fila los representan cambia. Es irreducible —esas filas son indistinguibles en esa columna— y está declarado en la documentación.
El modo aproximado dice qué métrica aproximó, y con qué función
perfilar_dbi(modo = "aproximado") sondea las funciones aproximadas nativas del motor y cae a la medida exacta cuando el motor las rechaza. Esa contabilidad ya estaba completa por métrica en resumen_tabla$sql —estado, metodo y error_esperado—, pero al imprimir el perfil se leía Métricas: calculado 24, estimado 6 sin saber cuáles eran las seis ni con qué función se calcularon.
Ahora el print lo dice: Aproximaciones aplicadas: distintos por APPROX_COUNT_DISTINCT y mediana por approx_quantile, o bien Sin aproximación para distintos y mediana: el motor no la aceptó y las métricas se midieron exactas. No cambia ningún número: publica donde se lee lo que ya estaba medido.
error_esperado sigue respondiendo desconocido cuando el motor no documenta una cota. Medir un error sobre una tabla no autoriza a declararlo como garantía sobre todas.
Los motores reales se verifican en cada push
La fila «probado contra el motor real» de la tabla de motores dependia de que alguien se acordara de correrlo a mano: se media una vez, quedaba en notas, y la tabla envejecia sin avisar.
benchmark/verificar_motor.R comprueba contra un motor real que las columnas se perfilan, que el dialecto se resuelve por sonda, que la media del motor coincide con la de R sobre la misma columna -que corra no alcanza-, que la clave primaria se lee del catalogo, y que la cobertura existe. Se configura por variables de entorno, asi que el mismo guion sirve en una maquina y en integracion continua, y sale con codigo distinto de cero si algo falla.
El flujo motores.yaml lo corre contra PostgreSQL 16 y MariaDB 11 levantados como servicios. Los que exigen un cliente propietario se siguen verificando a mano, y su fila de la tabla dice con que fecha.
El catalogo entra en la identidad de una tabla
catalogo.esquema.tabla es el nombre de tres partes que usan SQL Server y otros motores, y lupa perdia el del medio: se quedaba con la primera y la ultima. Como las dos que quedaban eran sintacticamente validas, el SQL salia bien formado contra una tabla inexistente y el error volvia al usuario como un problema suyo de permisos —lo mandaba a pedirle al DBA acceso a una tabla que ya podia leer—.
Ahora el catalogo es una columna estructurada, siempre presente y NA cuando no existe. El identificador une las partes que hay y sigue siendo inyectivo: t, esq.t, cat.esq.t y cat.t son cuatro identidades distintas.
Y la unicidad pasa a mirar la identidad completa. Antes, dos tablas con el mismo nombre y esquema en catalogos distintos colapsaban y la coleccion las rechazaba como repetidas: se rechazaba una frontera valida.
Lo que no cambia: sin catalogo el identificador es identico al de antes, asi que ninguna coleccion guardada deja de cruzar con su frontera; y un identificador mal formado -cuatro partes, comillas sin cerrar, punto inicial o final- se sigue rechazando nombrando la causa.
Una clave declarada excluye Benford, y queda declarado
Sobre una clave primaria se emitia una desviacion de Benford: se le afirmaba un problema de calidad a una columna que no tiene distribucion que analizar.
Se intento adivinar cual columna era clave por la forma de sus valores y ese camino se retiro, porque el criterio terminaba dependiendo de cuantas filas se habian cargado y callaba magnitudes reales. Declarada no hay nada que adivinar: si se pasa clave a perfilar(), o se lee del catalogo de la base, Benford no corre sobre esas columnas y la cobertura lo declara. Sin declararla el comportamiento no cambia.
Y el motivo dice cual de las dos cosas paso. «Parece un identificador» es una inferencia del paquete; «la clave fue declarada» es un hecho que trajo el usuario, y publicar la primera cuando corresponde la segunda le atribuye al paquete una deduccion que no hizo.
Menos codigo repetido, y el que quedaba ya habia divergido
Seis reglas estaban escritas mas de una vez. Dos de ellas tenian que coincidir o el paquete mentia, y una ya habia divergido en tres cosas a la vez.
- La identidad de una tabla en una coleccion estaba en dos lugares. La frontera se declara con una y se lee con la otra, asi que si divergian la cobertura de una coleccion dejaba de cuadrar.
- La validacion del destino de un archivo, en tres. Ya diferian en el orden de las comprobaciones, en si el mensaje nombra el directorio que falta —una no lo nombraba— y en la redaccion del aviso de sobrescritura.
-
datosdebe heredar de data.frame, once veces.perfildebe corresponder a las columnas, seis, con dos redacciones que convivian en el mismo archivo a trescientas lineas de distancia. - La interpretacion de numeros escritos como texto —convencion decimal, unidad y moneda— estaba dos veces, cuarenta y ocho lineas cada una. Las dos tienen que dar el mismo veredicto o la misma columna se describiria distinto segun por que camino se la miro.
- Los dos metodos de correctitud referencial eran la misma funcion escrita dos veces: 48 de sus 68 lineas coincidian y lo unico que cambiaba era que columnas de la referencia se usan.
Los ejemplos no nombran organismos
El ejemplo de organizacion() y las pruebas de esa granularidad usaban el nombre de organismos reales. Un ejemplo de roxygen viaja al .Rd y al sitio publicado, y las pruebas viajan en el tarball. Ahora usan nombres genericos -"Organismo A", "Organismo B"-, que es lo que corresponde: la frontera de una organizacion la declara quien la conoce, y el paquete no tiene por que nombrar a ninguno.
Lo que no se evaluo llega a las cuatro puertas
Cuando lupa decide no correr un diagnostico lo anota con su motivo, pero esa tabla solo llegaba a quien mirara el perfil. Ahora llega a las cuatro puertas desde las que se trabaja:
-
perfilar_por()la lleva por grupo. Cada grupo se perfila por separado, asi 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 miro. -
planificar_limpieza()la lleva y la anuncia al imprimir el plan. -
comparar_perfiles()la consulta para no informar comoresueltoun diagnostico que simplemente dejo de correrse. -
perfilar_dbi()ya la tenia enresumen_tabla$cobertura.
Una gramatica de entidades HTML, no dos
La misma expresion estaba escrita dos veces: una para detectar entidades y otra para repararlas. Si divergian se detectaba lo que no se reparaba, o al reves, y ninguna de las dos avisaba. Es el mismo acoplamiento que tenian las tres copias de la generalizacion de patrones.
En Oracle la cadena vacia es el nulo, y eso se declara
Las mismas tres filas -"", NA, "x"- dan n_faltantes = 2 por Oracle y 1 por un motor que las distingue. La misma columna tiene una completitud distinta segun el motor, y no porque el dato cambie.
Es la semantica documentada de Oracle, y lupa la declara. Lo que no hay es una corrida contra un servidor Oracle real que la respalde: el catalogo de motores da las dos variantes de Oracle como esperado, y esta nota decia antes “medido contra Oracle Free 23 real” sin un log que lo sostuviera.
No es un defecto que se pueda arreglar -es la semantica del motor- pero callarlo si lo seria: quien compare completitud entre entregas de motores distintos leeria una diferencia que no esta en los datos. Queda en la cobertura del resumen, y solo en los motores donde corresponde.
La clave primaria se lee del catalogo cuando esta declarada
Sobre un data.frame no hay a quien preguntarle cual es la clave, y por eso sugerir_clave() ordena candidatas. En una base relacional esa pregunta ya tiene respuesta escrita: la clave primaria esta en el catalogo del motor. Ahi no hay nada que sugerir, hay que leerla.
Se resuelve en una sola consulta, elegida por el controlador y no probando una tras otra: probar hasta acertar gastaria un numero de consultas que depende del motor, y plan_perfilado_dbi() promete exactamente cuantas emite. Es el mismo criterio por el que la sonda del desvio gasta siempre dos aunque acierte en la primera.
Y se distinguen dos respuestas que se confundian. SQLite devuelve cero filas sin error para una tabla que no existe, asi que filtrando en el SQL “no declara clave” y “no se pudo preguntar” llegaban iguales. Se piden todas las columnas y se filtra despues: cero filas significa que la tabla no esta, y filas sin ninguna marcada significa que no tiene clave.
Lo que no se evaluo se dice donde se decide, y la clave se pregunta
Una comprobacion de no-ASCII que la suite si puede hacer.
R CMD checkavisa cuando un archivo deR/trae un caracter no ASCII fuera de un comentario, y la suite no podia verlo: el barrido que existia mira los literales del espacio de nombres ya cargado, donde una cadena escrita con acento crudo y otra escrita con\uXXXXson la misma cadena. La unica forma de distinguirlas es mirar el fuente. La comprobacion nueva lo hace, se saltea diciendo por que cuandoR/no esta a la vista -bajoR CMD check, donde el trabajo lo hace el check nativo-, y se comprobo que falla: se le introdujo un acento crudo a proposito.El plan de limpieza dice lo que no se evaluo. El plan se arma desde los hallazgos, asi que por construccion no puede tener una accion para un diagnostico que se declino. Quien trabajaba desde el plan leia tres acciones y concluia que lo demas estaba bien, cuando lo que habia pasado es que sobre esa columna no se miro. Ahora la cobertura viaja con el plan y se anuncia al imprimirlo, con el motivo medido de cada diagnostico.
Una clave declarada que repite es un hallazgo, no un aviso. Se informaba con
cli_warn, que no tiene severidad, no viaja al informe ni al plan y no dice que filas repiten. Ahora es un hallazgo de severidaderrorcon la trazabilidad a las filas.-
sugerir_clave()yelegir_clave(). Adivinar si una columna es clave mirando solo sus valores no funciona -un monto tambien es casi unico-. Preguntar si funciona, y preguntar bien es ofrecer las candidatas ordenadas en vez de una casilla en blanco. El orden combina tres senales que se publican por separado, para poder discutirlo en vez de aceptarlo: si identifica cada fila, si no tiene ausentes, y cuanto se parece su nombre al de una clave.Otrapermite escribir una que no este en la lista, o varias para una clave compuesta. En sesion no interactiva no pregunta ni elige sola.Se distinguen dos motivos que antes se confundian: una columna que repite tiene duplicados de carga o la clave es otra; una que no esta en todas las filas esta incompleta. Piden arreglos distintos.
Dos arreglos medidos y retirados, y los limites que quedan declarados
Se probaron dos reglas para cerrar dos falsos hallazgos conocidos. Las dos funcionaban sobre el caso que las motivo y las dos callaban algo real, asi que ninguna entra: la regla del paquete es que una guarda solo se acepta si no calla nada verdadero. Lo que se midio queda escrito en el codigo, y los limites quedan fijados en test-ronda124.R para que esten declarados y no se redescubran.
-
Reconocer una clave dispersa por unicidad. El problema es real: sobre una clave repartida en un rango ancho se emite
desviacion_benford, que afirma un problema de calidad sobre una columna que no tiene distribucion que analizar. Y la densidad no puede resolverlo, porque una clave de 1 a 2.300.000 tiene densidad 0,0043, mas dispersa que un monto (0,0096).El criterio probado fue “unicidad que el azar no explica”. Fallo por dos motivos medidos. Primero, depende de cuantas filas se cargaron: el estadistico crece con el cuadrado de las filas, asi que la misma clave de cedulas cambia de veredicto al pasar las ~5.200 filas, y un padron de 2.000 filas recibe
desviacion_benfordmientras el mismo padron con 30.000 se reconoce bien. Eso es una propiedad de la consulta, no del dato. Segundo, calla magnitudes reales: una lectura acumulada de medidor, un timestamp en segundos, una coordenada UTM redondeada y un monto que solo se llena en algunos expedientes son unicos por su mecanismo, y sobre la lectura de medidor con un valor absurdo adentro el criterio se tragaba el valor absurdo. -
Descartar el centinela que tiene vecinos. La idea era que un centinela esta solo y un codigo de catalogo vive en un tramo, y sobre
222entre221y223funcionaba. Lo tumbo la codificacion mas comun de los microdatos de encuesta:9999= “no sabe” junto a9998= “no contesta”. Medido, el9998contaba como vecino y descartaba al9999, y como el9998no tiene forma de digito repetido tampoco entraba por su cuenta: los dos centinelas quedaban invisibles y salian cincuenta y cinco codigos de ausencia informados como valores extremos de una magnitud.No hay senal de forma que separe
221/222/223de9998/9999: en los dos casos son valores contiguos, extremos y repetidos. La diferencia es semantica. Una comprobacion que no podia fallar. La prueba que cuida que una columna protegida no publique el valor centinela se salteaba a si misma cuando el hallazgo no aparecia: si el detector dejaba de emitir, la suite quedaba verde. Ahora asevera que el hallazgo esta.
Once senales falsas sobre una base real, y ninguna se apaga en silencio
Una corrida contra tres tablas administrativas reales dio 24 senales, de las cuales once eran falsas. El calculo estaba bien en las once; lo que fallaba era el juicio de si la prueba corresponde. Un identificador no es una magnitud, y hay pruebas que solo describen magnitudes.
-
Benford y limites de Tukey sobre numeraciones (seis falsos). La guarda que ya tenia Benford exigia una corrida consecutiva sin huecos, y un identificador real tiene huecos: los que se dieron de baja. Un
MotIdde 1 a 4557 sobre 3.159 filas no la pasaba y Benford se corria igual.Lo que separa una numeracion de una magnitud no es la unicidad -un monto tambien es casi unico- sino la densidad: un identificador ocupa un tramo compacto de los enteros (0,69 en ese caso) y una magnitud se reparte por varios ordenes (0,00005 para montos entre 9 y 9.999.999).
Un valor fuera de escala rompe esa compacidad, asi que los casos que hay que ver se siguen viendo: un
10000entre identificadores de 1 a 100, o un ano centinela 1900 entre anos 2000-2030, bajan la densidad y vuelven a senalarse. alta_cardinalidadsobre texto libre (tres falsos). Un nombre, una descripcion o un objetivo tienen cardinalidad alta por definicion. Ahora una columna de texto cuyos valores promedian 40 caracteres o mas no se marca.Relacion de orden entre dos numeraciones (un falso).
MotId <= MEsIdse cumplia en el 99,1 % de las filas porque los dos contadores avanzan juntos, y el 0,9 % restante “violaba” una regla que no existe. Lo que separa ese par de uninicio/finlegitimo -tambien entero, tambien denso- es la brecha: constante fila a fila cuando hay una regla detras, erratica cuando solo hay dos contadores. La guarda respeta el rescate por brecha estable que ya existia.constantemedido sobre una muestra (un falso). Una tabla de 200 filas con tres valores, muestreada en 50, informaba “la columna contiene un unico valor”. Una proporcion estimada sobre una muestra sigue siendo honesta; una cuantificacion universal no: basta una fila no leida para desmentirla.
Ninguno de los seis se apaga en silencio. Bajar el ruido callando seria mejorar el numero sin mejorar el paquete, asi que cada prueba que no se corre deja su fila en cobertura_diagnosticos, con el motivo medido -que porcentaje de los enteros cubre la columna, cuantos valores se habrian senalado, cuantas filas de cuantas trae la muestra- y que hacer si el usuario no esta de acuerdo. Los pares de orden descartados quedan nombrados en meta$orden_columnas$pares_identificador_descartados.
La ganancia por hilos no era una propiedad del paquete
La vinieta de escala publicaba una tabla -133,28 s con dos hilos, 70,31 con dieciseis- medida sobre un padron que no se distribuye, asi que nadie podia rehacerla. Ahora benchmark/medir_escala_hilos.R genera un padron sintetico y mide la misma curva, y no da lo mismo: sobre 100.000 filas con 23.800 nombres distintos, de dos a dieciseis hilos se gana un 12 %, no la mitad del reloj.
La explicacion es que los hilos los usa stringdist al comparar, y generar los candidatos y armar los grupos no los usa. Segun cuanto pese cada parte en un conjunto concreto, la ganancia va del 90 % al 12 %. La vinieta publica las dos tablas y dice que quien vaya a subir el valor conviene que mida su caso.
Lo que si vale en las dos: pasados dieciseis hilos no hay ganancia medible, y el resultado no cambia. Y esto ultimo recien ahora esta comprobado de verdad. El banco comparaba cuantos pares devolvia cada configuracion, y como el tope de resultados se alcanza en todas, el numero era siempre el mismo: la comprobacion se cumplia sola. Ahora compara los 50.000 pares uno por uno.
Otros dos defectos del mismo banco, encontrados antes de que publicara ningun numero: el generador daba 80 nombres distintos para 100.000 filas -el detector trabaja sobre formas distintas, no sobre filas, asi que no ejercitaba nada- y nrow() sobre el resultado devolvia NULL, porque es una lista con $pares y no un data frame.
Un diagnostico nuevo: el centinela que ninguna lista puede declarar
Un 9999 es una edad imposible y un codigo postal perfectamente valido. Por eso la lista de sentinelas_numericos no lo trae por omision, y hace bien: marcarlo siempre romperia cualquier columna donde ese numero es un dato.
Lo que si lo distingue es cumplir las tres cosas a la vez: quedar fuera de los limites de la columna, repetirse cinco veces o mas, y tener forma de digito repetido. posible_centinela_numerico lo informa sin contarlo como ausencia -esa decision es de quien conoce la columna, agregandolo a la lista- y nombra las filas donde esta.
Medido sobre doce columnas con la respuesta conocida, acierta las doce. Los tres controles que podrian haberlo roto: un codigo postal 9999 repetido treinta veces no es extremo dentro de su columna, un monto real de 9999 no se repite, y un ano 1999 no tiene esa forma.
Es el unico diagnostico del paquete que ninguna senal sola podia dar: la forma sin los limites marca codigos validos, los limites sin la forma marcan cualquier extremo, y la repeticion sin las otras dos marca cualquier valor frecuente.
Un valor centinela se escapaba de la proteccion de datos personales
Es lo mas serio de la tanda y lo introdujo la tanda misma. El perfil pasa a medir cual es el valor que una columna usa para decir “sin dato” -un 9999 entre edades- y ese campo publicaba un valor de celda sin pasar por el enmascarado. Sobre una columna de documentos protegida se veia asi:
moda [valor protegido]
minimo NA
centinela_valor 9999
Y la descripcion del hallazgo lo nombraba en texto, con lo cual llegaba hasta el informe HTML. La evidencia si salia protegida: la fuga entraba por la puerta de al lado.
Ahora centinela_valor se enmascara igual que el minimo, la moda y la media, y la descripcion se limpia en la misma capa que ya limpiaba la evidencia. Las repeticiones se siguen informando, porque son un conteo y no un valor.
Que el valor sea casi seguro un centinela y no un documento no cambia la regla: la proteccion no adivina cuales valores son inocentes.
Dos diagnosticos que se contradecian sobre la misma columna
999 esta en la lista de centinelas declarados, asi que faltantes_disfrazados lo cuenta como ausencia con severidad error. El diagnostico nuevo decia, sobre esa misma columna, que “no se cuenta como ausencia porque no esta declarado”. Los valores que la lista ya cubre dejan de producirlo: el diagnostico existe para los que no estan declarados, como un 8888.
perfilar() dentro de do.call() rompia el informe
deparse() de una expresion larga devuelve varias lineas, y con do.call(perfilar, list(tabla, ...)) la expresion es la tabla entera: el nombre del conjunto salia con ocho elementos y reportar() fallaba con “values must be length 1”. Pasar los argumentos en una lista es lo natural cuando se perfila en un bucle. Si la expresion no cabe en una linea, no la escribio nadie y se usa una etiqueta generica.
Lo que el idioma no deja resolver
La comparacion que separa una errata de la misma palabra escrita de otra manera ignora acentos, y en español el acento distingue palabras. El mismo mecanismo que junta Jose con José -que si es la misma- junta papa con papá y ano con año, que no lo son. Separarlos pide un diccionario del idioma.
Asi que se corrigio la afirmacion, no la medida: el texto decia “son la forma dominante escrita distinto” y ahora dice en que difieren, que es verificable, y avisa de que en español el acento puede cambiar la palabra.
La misma numeracion, descrita de dos maneras
Aparecio al intentar romper el arreglo de arriba. Un codigo 1..284 con 179 valores fuera de los limites de Tukey:
- guardado como
integer, se callaba y quedaba declarado en cobertura; - guardado como
double, se senalaba.
La guarda dependia de como estaba almacenado el numero, no de que es. Y eso pesa mas de lo que parece: por la puerta DBI casi todo llega como doble -SQLite entrega asi hasta las fechas- y varios lectores de CSV tambien, asi que el arreglo no cubria el caso que lo motivo cuando el identificador venia de una base.
La causa estaba en la raiz. .resumen_secuencia_entera exigia tipo entero y dos lineas mas abajo comprobaba que todos los valores fueran enteros. La segunda condicion es la real; la primera solo dejaba columnas afuera, y con ellas su densidad_secuencia_entera, que quedaba en NA.
Lo que dijeron las cinco auditorias externas
Cinco revisiones independientes -coherencia entre las dos puertas, prueba de las afirmaciones publicadas, barrido de codigo, bancos y redaccion- dejaron sus informes. Lo que sigue es lo que quedaba abierto de ellas.
La tasa de referencia no era una tasa del motor.
supuesto_costodecia “unos cinco millones de lecturas de fila por segundo sobre PostgreSQL 16”, y ese cociente esta en las unidades que cuenta el plan, no en filas que el motor haya leido: la cuenta supone que ningun indice ayuda y cobra el desvio como dos pasadas aunque un motor con desvio nativo lo resuelva en una. El numero sirve para lo que existe -convertirfilas_leidasen segundos- y ahora lo dice. Se midio: 26.001.000 lecturas en las unidades del plan sobre 5,3 segundos.La tasa de pares se remidio con el banco. Da de 660.000 a 1.150.000 pares por segundo con valores de cuarenta caracteres -la banda cubre dos maquinas- y unos 80.000 con valores de doscientos, contando los pares que se comparan de verdad. Con doscientos caracteres el detector recorta por
max_trabajoa 1.005 formas de 2.000, asi que dividir por los pares que el plan contaria inflaba la cifra cuatro veces.Las dos puertas describen tipos distintos, y ahora se declara.
perfilar_dbi()informa el tipo que declara el motor, y un motor que no preservaDATEniBOOLEANhace que esas columnas se midan como numeros: la misma columna de fechas da el desvio en dias por una puerta y en segundos por la otra, y la moda como entero crudo en vez de fecha formateada. Cada puerta describe lo que tiene delante; lo que faltaba era decirlo.El desvio de una columna temporal esta en segundos y no lo decia ninguna parte. Los demas momentos viajan formateados en
minimo_fecha,media_fechay compania; el desvio no es un momento sino una duracion, queda como numero, y un136610.4sobre fechas son 1,6 dias.Cuatro funciones internas que solo llamaba la suite se sacaron del paquete. Viajaban en cada instalacion sin que nada las usara, y sus pruebas daban la impresion de que estaban vivas. La unica que servia de algo -una version escalar contra la que medir la vectorizada- sigue existiendo como oraculo dentro de su propia prueba.
Dos etiquetas exigian saber estadistica para poder descartarlas. Un hallazgo que hay que traducir antes de juzgarlo cuesta mas que uno que se entiende: quien no sabe que es Benford no lo puede descartar con criterio, o le cree. Ahora
desviacion_benfordexplica que en muchas magnitudes el 1 encabeza cerca del 30 % de los valores y el 9 menos del 5 %, youtliersdice que hay valores muy alejados del grueso de la columna antes de nombrar a Tukey.
Una prueba que pasaba sin probar nada
test-formatos-adicionales.R verifica que los meses escritos se detecten igual en cualquier LC_TIME. Recorria tres locales con try(Sys.setlocale(...)) y comparaba contra la base medida en C. En una maquina donde esos locales no estan generados -una imagen minima de contenedor no los trae- la llamada falla, el try se traga el fallo, el locale nunca cambia y el test compara el resultado contra si mismo.
Pasaba. Y la unica senal de que no habia medido nada era un aviso suelto en el resumen de la suite: “OS reports request to set locale cannot be honored”.
Ahora comprueba que el locale quedo puesto, y si ninguno se puede poner se saltea declarando el motivo en vez de contar un exito vacio.
El plan de consultas dice rango en todas partes
attr(plan, "supuesto") ya declaraba un rango, pero el metodo de impresion, el @return de la ayuda, los dos README y la vinieta seguian diciendo “techo”. Un techo que el propio objeto desmiente dos lineas mas abajo es peor que no decir nada.
-
print()ahora dice “entre N y M consultas”, y sobre un plan subconjuntado -que conserva la clase y pierde los atributos- imprime la tabla y avisa, en vez de titular “sin dato consultas sobre sin dato filas”. -
total_lotes_rechazadosaparece en el@return, que no lo documentaba.
Una tabla con acentos rompia el perfil entero
Es el defecto mas serio de la tanda, y lo introdujo el arreglo del orden del vocabulario de ayer. Sobre cualquier tabla leida con read.csv() que tuviera acentos:
Error: Character encoding must be UTF-8, Latin-1 or bytes
El orden por bytes de R rechaza una cadena marcada unknown que contenga bytes no ASCII, aunque sean UTF-8 perfectamente validos, y asi llega cualquier CSV en espanol por el camino mas comun que hay: "Combustibles liquidos", "Energia Electrica". El mismo error estaba en el desempate de la moda.
- Ahora la codificacion se marca antes de ordenar. Lo que despues de eso siga sin ser valido pasa por
iconv(sub = "byte"): no es bonito, pero es determinista y ordenable. Caer al orden del entorno habria devuelto la dependencia de la maquina que este orden existe para sacar. - Los otros tres usos de orden por bytes del paquete ordenan enteros, que no tienen requisito de codificacion.
-
Ni las cuatro auditorias externas ni las 15.696 comprobaciones de la suite lo encontraron, porque todos los fixtures son ASCII. Aparecio buscando otra cosa: el registro publico con el que se cierra una fila de la tabla de evidencia. La prueba nueva construye la cadena con
rawToChar(), porque un literal en el fuente lo marca el parser de R y el caso no se ejercita.
La tabla de evidencia dice ahora con que se reproduce cada fila
Llego a publicar tres numeros que nadie podia comprobar desde el repositorio. El problema de fondo no eran los tres numeros sino que la tabla no obligaba a que cada afirmacion tuviera un reproductor. Ahora tiene una columna que lo dice.
-
Controles limpios: decia 43 tablas y 25 senales. El generador esta en el repositorio, se redujo a 31 tablas y el ruido bajo a 8 -el paquete mejoro y el texto seguia diciendo lo viejo-. Los tres numeros quedan fijados en
test-ronda107.R. - Defectos plantados: se saca la fila. El numero es real, se midio en tres rondas, pero su banco no esta en el repositorio y reconstruirlo de memoria daria nueve defectos parecidos y no los mismos. Vuelve cuando exista su test.
-
Registro real de sanciones: ahora hay
benchmark/medir_sanciones.R, que baja el registro publico del catalogo nacional -2.556 filas- y contrasta cada hallazgo de severidaderrorcontra una comprobacion escrita a mano en R base. Da 9 de 9, uno mas que cuando se midio. Ese archivo es ademas la regresion del defecto de codificacion de arriba: es el que lo destapo.
Un token que es marca de formato ya no genera un falso duplicado
Mirando que reportaba el detector de vocabulario sobre una tabla real de vuelos aparecieron dos familias mezcladas:
[1:48 p.m. (27) / 1:48 p.m. Delayed (1)] <- el estado del vuelo pegado
[12:00 a.m. (5) / 12:00 p.m. (42)] <- doce horas de diferencia
La primera es un hallazgo real. La segunda es un falso positivo: 12:00 a.m. y 12:00 p.m. son dos valores legitimos distintos, y no hay forma de saber mirando la columna cual fue tipeado mal. Marcarlos a todos no es detectar, es sospechar en bloque de todos los valores de una forma y acertar por casualidad los que estaban mal: la precision de ese diagnostico era 0,259, tres de cada cuatro marcados eran valores correctos.
- El detector descarta un par cuando todos los tokens que lo distinguen aparecen en buena parte de la columna.
a.m.yp.m.estan en casi todos los valores;Delayedesta en uno. - Tres condiciones lo acotan, y las tres salieron de romper la suite con una version que no las tenia (44 pruebas caidas): el valor tiene que tener mas de un token —si no, el token que difiere es el valor entero y la regla borra el caso central del detector—, la cantidad de tokens tiene que coincidir —cuando cambia, como al pegar
Delayed, hay que conservarlo— y el vocabulario tiene que tener al menos 20 formas, porque “aparece en toda la columna” no significa nada sobre cinco. - El descarte se declara en
n_pares_descartados_formato. -
El costo esta medido y publicado. Sobre el banco de vuelos la precision sube de 0,524 a 0,658 y la cobertura baja de 0,281 a 0,238: se pierden 111 aciertos porque el banco inyecto erratas que son exactamente un cambio de meridiano. Esas caen debajo del techo estructural —un
p.m.mal tipeado es indistinguible de uno correcto sin una referencia externa— y el lugar correcto para atraparlas es una regla entre columnas, no la proximidad de cadenas. - Sobre el banco de hospitales no cambia nada: lo que distingue dos nombres es contenido y no una marca de formato. La regla actua solo donde la marca existe.
Una columna en Latin-1 perdia sus acentos en silencio
Es el defecto mas grave que encontro esta tanda, y no es un caso de borde: es un CSV viejo en espanol, que es la mayoria de lo que hay en datos publicos de la region. Sobre una columna con cinco valores distintos, el perfil informaba:
n_distintos: 2 <- son 5
n_faltantes: 0 <- dice que no falta nada
cobertura: 0 filas <- no declara nada
validUTF8() mira los bytes, y los de un texto marcado latin1 no son UTF-8 validos, asi que CAFE, ANO y NUMERO con tilde se volvian NA antes de llegar a cualquier diagnostico. El invariante del paquete roto en su forma mas directa: informar como medido lo que se descarto.
-
Lo que R sabe convertir ahora se convierte.
Encoding()dicelatin1cuando R conoce la codificacion, yenc2utf8()convierte sin perder nada. El paquete estaba tirando informacion que podia recuperar con una llamada. Con eso, la misma columna informan_distintos = 5. -
Y lo que no se puede convertir, se declara. Un texto cuya codificacion nadie declaro y cuyos bytes no son UTF-8 validos se sigue descartando —no hay forma de adivinar si
0xE9era unaecon tilde o basura— pero ahoracobertura_diagnosticosgana una filatexto_no_descifrableque dice cuantos valores quedaron afuera, y por que no cuentan ni como distintos ni como faltantes.
La proteccion de datos personales dependia de por que puerta entraras
La media de una columna de cedulas salia expuesta por perfilar() y tapada por perfilar_dbi():
perfilar() media = 5108024 detalle: [estadisticos de orden protegidos]
perfilar_dbi() media = NA detalle: [estadisticos de orden y momentos protegidos]
El argumento estaba escrito del lado DBI desde antes —“la media de las cedulas de una tabla chica reconstruye demasiado”— y el camino principal no lo aplicaba. Ahora las dos tapan la media, y el texto distingue si se taparon estadisticos de orden, momentos o los dos, para no declarar una proteccion que no se aplico.
El total del plan no era un techo, y ahora se publica como rango
attr(plan, "supuesto") decia que el total era un techo y declaraba solo la direccion “menos”: una columna sin valores validos no emite sus metricas. Nunca declaraba la direccion “mas”. Medido contra un motor que rechaza lotes —el caso exacto que motivo la consolidacion—:
plan (techo) = 22 consultas real emitidas = 30
Si un lote falla, se emite la consulta del lote y ademas una por columna. Quien decide la viabilidad de una corrida con ese numero se quedaba corto justo en el escenario de degradacion. Ahora el plan publica total —lo que cuesta si ningun lote se rechaza— y total_lotes_rechazados —si se rechazaran todos—, y el costo real cae entre los dos.
Dos numeros publicados que no resistieron que otro los midiera
Los dos eran nuestros y de esta semana, y los dos son la misma forma de error: medir una cosa y publicarla como otra.
-
“Una columna corriente de dos mil valores se compara entera” es falso sin calificar el largo. El tope por trabajo muerde cuando
L^2 x n(n-1)/2supera2e10, o sea a partir de 101 caracteres para dos mil valores distintos. Y falso en el peor lugar: la columna de WKT de 900 caracteres que motivo el presupuesto cae del lado recortado, asi que la frase tranquilizadora no alcanzaba justo a los datos que hicieron falta el tope. -
La tasa de 200 caracteres estaba inflada cuatro veces. El banco dividia el tiempo por los pares que el plan contaria y no por los que de verdad se compararon: sobre 2000x200 el plan cuenta 1.999.000 pares y se comparan 499.500, porque
max_trabajorecorta a 1.000 formas. La tasa real es unos 70.000 pares/seg, no “70.000 a 270.000”.
La tabla de evidencia del README, medida de nuevo
Publicaba 43 tablas de control y 25 senales. El generador esta en el repositorio, se redujo a 31 tablas, y el ruido bajo a 8 —el paquete mejoro— pero el README siguio publicando los numeros viejos porque ninguna prueba los ataba. Ahora dice 31 tablas, 0 errores y 8 senales, y los tres estan fijados en test-ronda107.R: si cambian, la suite falla y hay que actualizarlos a proposito.
Se saco la fila de los nueve defectos plantados. El numero es real —se midio en tres rondas— pero el fixture no esta en el repositorio, y reconstruirlo de memoria daria nueve defectos parecidos y no los mismos. En la tabla que sostiene el argumento del paquete, una fila menos es mejor que una fila que no se puede comprobar. Vuelve cuando exista su test.
Restos de correcciones anteriores
- Una trazabilidad sin filas se declaraba disponible: el condicional tenia las dos ramas iguales (
if (total) "disponible" else "disponible"), resto de una correccion. Con cero indices el objeto prometia una localizacion que no existe, conlocalizador = "ninguno"al lado. La rama vacia va ano_disponible, que es el valor por omision de la propia funcion. -
.reparar_mojibake_unoestaba definida dos veces con algoritmos distintos y topes distintos (4 y 20 iteraciones). El orden alfabetico de carga decidia cual corria; la otra era codigo muerto que alguien podia “arreglar” creyendo que era la que se usa. -
.bit64_disponibley.bit64_disponible_dbieran la misma funcion con dos nombres. Queda un solo punto de verdad. -
.detectar_orden_columnas()recibia un argumentoresultadosque su cuerpo no usaba, y el llamador lo construia para nada. -
muestra = 1.5se aceptaba en memoria y perfilaba una fila en silencio, mientras la via DBI daba error. Ahora las dos lo rechazan. La unica diferencia que queda es deliberada:Infvale en memoria y no contra un motor.
El veredicto ya no depende de como venga ordenado el archivo
Cuando el vocabulario de una columna de texto excede el presupuesto, hay que elegir que formas comparar. Se elegian las primeras en aparecer, y eso hacia que el resultado dependiera del orden de las filas.
Medido sobre la columna nombre de Ejes de vias de circulacion de Montevideo —45.400 filas, 8.318 formas distintas, del catalogo nacional de datos abiertos—, las mismas filas daban:
| orden de las filas | grupos de casi-duplicados |
|---|---|
| tal como viene el archivo | 26 |
| desordenado (semillas 11, 202, 7777) | 71, 85, 70 |
| alfabetico | 148 |
De 26 a 148 segun como estuviera ordenado el archivo. Un perfilador que hace eso mide la forma fisica de la tabla, no los datos, que es exactamente lo que el paquete promete no hacer.
- Ahora las formas se ordenan antes de recortar. Los cinco ordenes de arriba dan 148 grupos: el resultado es el mismo venga como venga el archivo.
- Ordenar tiene ademas una razon de fondo: los casi-duplicados quedan adyacentes —
CAMINO CARRASCOjunto aCAMINO AGRARIOS—, asi que el corte cae entre familias en vez de partirlas. Una muestra al azar rompe pares: si de un grupo de dos sobrevive uno, el grupo desaparece. Por eso el azar rinde 70-85 y el orden rinde 148 con el mismo presupuesto. - El orden es por bytes (
method = "radix") y no el del entorno: la intercalacion por omision cambia de una maquina a otra, y eso habria cambiado el defecto de lugar en vez de sacarlo. - El mensaje de cobertura dice ahora que las formas comparadas son las primeras del alfabeto y que lo que queda afuera es su tramo final. Antes recomendaba “desordenar la tabla antes”, que era el mejor consejo posible mientras el defecto estuviera.
Lo destapo la tercera vuelta contra bases reales, que dejo esta afirmacion sin verificar por no encontrar una columna que la ejercitara. La columna existia.
Un conteo del perfil ya no cambia de clase segun que tenga instalado el usuario
perfilar_dbi() devolvia n_validos, n_faltantes, n_distintos, frecuencia_moda, meta$filas y filas_totales_fuente como integer64 siempre que bit64 estuviera instalado, incluido un conteo de 20.
Eso no agregaba precision: por debajo de 2^53 un
doubleya representa el entero exacto.Y agregaba tres problemas. La clase del mismo campo dependia de un
Suggests.perfilar()devolviaintegerparan_distintosyperfilar_dbi()devolviainteger64: dos puertas del mismo paquete en desacuerdo sobre el mismo campo.-
El tercero es el que decide. Un perfil guardado en una maquina con
bit64y leido en una que no lo tiene mostraba:columna n_validos n_distintos 1 id 9.881313e-323 9.881313e-323donde midio
20. Sin error, sin aviso, y sumando como si fuera un numero. Informar como medido algo que no lo es, en el paquete cuyo argumento es justamente ese. Los conteos salen ahora
numeric.integer64se conserva solo donde compra exactitud: un conteo por encima de 2^53 que el motor entrega como texto o comointeger64. Para un conteo de filas eso significa una tabla de mas de nueve mil billones.De paso se corrige el error simetrico:
conteo_exactodeciaFALSEpara un conteo entregado como texto por encima de 2^53, que es justo el caso donde si se guarda exacto. El paquete se declaraba menos preciso de lo que era.
El R minimo declarado ahora es un R medido
DESCRIPTION declaraba R (>= 3.6.0), y era una promesa que el paquete no cumple. Pasa a R (>= 4.1.0).
- Se venia afirmando que la suite no puede correr bajo R 3.6, porque
testthatdeclaraR (>= 4.1.0). Eso es cierto contra CRAN de hoy y falso contra el snapshot de la epoca: ahitestthat 3.1.7instala sin problema. La suite corre, y da[ FAIL 18 | PASS 15356 ]. La generalizacion tapo dieciocho fallos. -
Seis de los dieciocho salen de una sola causa. Bajo R < 4.0,
data.frame()traestringsAsFactors = TRUE, y el paquete tiene 275 llamadas adata.frame()que no lo declaran: ahi las columnas de texto nacen factor. No es cosmetico: la proteccion de datos personales escribe"[valor protegido]"en una columna factor, R lo rechaza por nivel invalido y quedaNA. No hay fuga, pero la promesa sobre esa celda no se cumple, y falla en silencio. Bajo R 4.0.5 con los mismos paquetes esos seis desaparecen. -
4.1.0y no4.0.0porque es lo que pidetestthatactual: en el piso declarado la suite corre con las herramientas de hoy, que es lo que hace verificable la promesa. Medido en contenedor antes de declararlo:rocker/r-ver:4.1.3,checking tests ... OK,[ FAIL 0 | PASS 14613 ]. Nada obligaba al numero viejo:cli, el unico Import, pideR (>= 3.4). - Los doce fallos restantes bajo versiones viejas de los
Suggestsno son del R: conRSQLite 2.3.1ybit64 4.0.5los conteos vuelven comointeger64y viajan asi hasta el perfil, asi que un mismo campo cambia de clase segun que tenga instalado el usuario. Queda anotado como trabajo abierto, no como resuelto.
Tres pruebas que exigian justo lo que decian no tener
Las tres tenian la misma forma, y solo se ven en un entorno donde el paquete opcional de verdad no esta.
- La normalizacion Unicode juntaba en un bloque la mitad que mide —que necesita
stringi— y la mitad que declara su ausencia, simulada con un mock. Sinstringi, la primera fallaba y se llevaba puesta a la segunda. Poner una guarda habria salteado las dos, incluida la que no la necesitaba: van separadas, con la guarda donde corresponde. - Las dos pruebas de
integer64_sin_soportearmaban eldata.framecon la columna ya marcada comointeger64, y eso obliga aas.data.frame()a buscar el metodo debit64. La prueba de que faltabit64exigia quebit64estuviera. Ahora la clase se pone despues de armar la tabla, que ademas es el camino realista: la columna llega marcada dentro de una tabla que ya existe. -
DBI::Id()acepta argumentos sueltos recien desde 1.2. Con la version anterior el error que salta es el de DBI y la prueba mide otra cosa; van nombrados.
Nota sobre el instrumento: _R_CHECK_DEPENDS_ONLY_=true ocultaba bit64 para skip_if_not_installed() —saltaba ocho pruebas por eso— y aun asi las dos de integer64 pasaban ahi, mientras fallaban en el contenedor. El check con dependencias recortadas no es equivalente a un entorno que no tiene el paquete.
El reintento que contra el driver real no se disparaba nunca
La version anterior agrego un reintento: si la lectura de la muestra falla y hay columnas de tipo largo declaradas, se reintenta sin ellas y se declara que quedaron afuera. Contra una base real no se disparo ni una vez. En la tabla que motivo el arreglo —158 columnas, 90 de ellas varchar(max)— el patron de tipos reconocio 0 de 90, la muestra se perdio entera y no aparecio el aviso de alcance: exactamente lo que el arreglo prometia evitar.
- La causa no es un tipo que falte en la lista. El driver no informa nombres:
odbcresuelvedbColumnInfo()connanodbc::result::column_datatype(), que devuelve el codigo numerico de ODBC. Contra{SQL Server}las noventa columnas llegan como noventa veces"-1". Comprobado en las dos puntas: la biblioteca compilada no exponecolumn_datatype_nameni contiene un solo literal de nombre de tipo SQL, y el patron de nombres no matchea"-1". - El arreglo se habia probado contra un banco que hablaba en nombres, que es la unica forma de tipo que el patron sabe leer. El banco compartia con el patron justo la propiedad cuya ausencia era el fallo.
- El reintento ya no infiere: pregunta. Cuando la lectura falla, se aisla por descarte cuales columnas no se pueden leer —biseccion sobre el conjunto, podando los subconjuntos que si se leen— y se declaran esas. Es independiente del controlador: funciona sin reconocer ningun tipo.
- El patron queda como atajo optimista: si reconoce el tipo, ahorra el descarte. Se le agregaron los codigos ODBC (
-1,-4,-10) y las variantes de nombre (LONG VARCHAR,SQL_LONGVARCHAR,WLONGVARCHAR) que tampoco veia. Pero la correccion ya no cuelga de el. - Lo que se declara cambia segun como se supo.
omision_comprobadadistingue medido de supuesto: por descarte, cada columna fallo sola y el resto se leyo junto, y el aviso lo dice; por el atajo, sigue diciendo “no se comprobo que sean la causa”. Ysondas_descartepublica cuantas consultas costo averiguarlo. - El descarte esta acotado por los dos lados: como mucho
2nsondas sobrencolumnas —que es lo que cuesta la biseccion en el peor caso—, tope absoluto de 512, y nunca mas de la mitad del saldo demax_consultas, para no recuperar la muestra a costa de quedarse sin presupuesto para el resto. - Si el descarte no aisla nada, no se inventa una culpable: la muestra se declara no disponible y el motivo dice cuantos subconjuntos se sondearon y como termino —ninguna falla sola, o fallan todas, o el tope corto antes—.
- De paso: el reintento rearmaba el SQL a mano y perdia el muestreo del motor por el camino —volvia a una lectura de primeras filas mientras
metodoseguia declarando el muestreo nativo—. Ahora la lectura original y el reintento salen de la misma receta, ymetodo,acotado_enyfraccionse corrigen con lo que de verdad se emitio.
Un plan que contaba la mitad del reloj
plan_perfilado_dbi() le ponia magnitud "baja" a una tabla de 3.912 filas que tardaba 35 segundos. Cada numero que informaba era cierto —64.592 lecturas de fila, cero ordenaciones— y el juicio era falso: el trabajo no estaba en el motor sino en R, comparando formas de una columna de geometria en texto. Medir una mitad y llamarla el total es informar como completo algo parcial.
- La magnitud se estima ahora en dos mitades. La del motor sigue en
filas_leidasyordenaciones_completas, resumida enmagnitud_motor. La del cliente esta encolumnas_textoypares_texto—cuantos pares de formas podria comparar el detector de vocabulario sobre la muestra—, resumida enmagnitud_texto.magnitudes la mayor de las dos. - La unidad es el par de formas comparadas, que es una cuenta y no un indice: la muestra trae
mfilas, las formas distintas son a lo sumom, y el detector nunca compara mas demax_parespor columna. El tope se lee de la firma del detector, no se copia, asi que no puede quedar estimando contra un numero viejo. - Los umbrales (2e6 y 2e8 pares) estan anclados a la misma escala de segundos que los del motor, con la tasa medida: de 660.000 a 960.000 pares por segundo sobre valores de cuarenta caracteres, contra los cinco millones de lecturas de fila por segundo de la referencia de PostgreSQL. La medicion esta en
benchmark/medir_costo_texto.R, seccion 5, para que el umbral no sea un numero elegido a dedo. - Lo que el plan no puede saber queda dicho, no escondido: el conteo de pares es exacto, pero cuanto cuesta cada uno depende del largo de los valores, que el plan no leyo. Sobre valores de doscientos caracteres la tasa cae a unos 70.000 pares por segundo, asi que con textos muy largos el tiempo real es varias veces el que sugiere la referencia.
supuesto_costolo declara en vez de prometer segundos. - La impresion muestra las dos mitades, y cuando la que pesa es la de R nombra la palanca de ese lado —
max_trabajo_vocabulario—, que las palancas del motor no tocan.
Presupuestos que miden trabajo, no que cuentan unidades
Una tabla del catalogo de PostGIS —spatial_ref_sys, 3.912 filas y 5 columnas— tardaba 243 segundos. No era la geometria: eran cadenas largas, WKT de proyecciones, y el detector de vocabulario se llevaba el 99,6 % del costo. Tenia dos topes, max_valores = 5000 y max_pares = 2000000, y ninguno de los dos miraba cuanto costaba cada unidad: 800 valores son 319.600 pares, muy por debajo del tope, pero cada comparacion era una Jaro-Winkler sobre 900 caracteres.
La unidad del presupuesto es ahora la comparacion de un caracter contra otro, que es el bucle interno de la distancia: comparar dos valores de largos L1 y L2 cuesta del orden de
L1 x L2. La suma sobre todos los pares de un prefijo sale exacta y en tiempo lineal, sin materializar la matriz.Contar pares por largo medio no alcanzaba. Medido, ese modelo compraba 5,3 millones de unidades por segundo con valores de 900 caracteres y 44 millones con valores de 40: ocho veces de diferencia es no tener modelo. Con el producto de largos la dispersion baja a 4,25 veces, y lo que queda es a favor de las columnas de valores cortos, que son el caso comun.
-
max_trabajo_vocabulariovale2e10por omision, calibrado contra la medicion y no contra la intuicion:valores largo sin tope con tope comparado 400 900 15,1 s 4,3 s 55,5 % 500 900 23,0 s 4,3 s 44,4 % 800 900 61,3 s 4,6 s 27,8 % 2000 80 5,0 s 5,1 s 100 % El ultimo renglon es el que importa tanto como el tercero: una columna corriente de dos mil valores no se recorta, siempre que sus valores midan menos de unos cien caracteres: el tope por trabajo muerde cuando
L^2 x n(n-1)/2supera2e10, o sea a partir de 101 caracteres para dos mil valores distintos. Tampoco 500x20 ni 1000x30. El riesgo del arreglo era romper el caso comun para arreglar el patologico. Una aclaracion que hay que hacer, porque la primera version de esta nota afirmaba de mas: 3000x20 si se recorta, pero no por el presupuesto nuevo sino por
max_pares, el tope viejo, que acota en 2.000 formas sin mirar el largo. Sigue puesto porque acota la memoria de la matriz de pares. El recorte se declara con su motivo, asi que no se pierde en silencio, pero decir “no se recorta” era falso. Salio de que el banco apagabamax_parespara aislar el efecto del tope nuevo y despues se leyo esa medicion como si fuera lo que recibe un usuario. El banco separa ahora las dos cosas.Lo recortado se declara, con las dos cuentas separadas: cuantas formas normalizadas quedaron sin comparar y cuanto trabajo era, en el alcance del hallazgo y en
cobertura_diagnosticos, junto con cual de los dos topes recorto. Si aprietan los dos, el motivo los nombra a los dos: el usuario tiene que poder elegir cual aflojar.El recorte toma las primeras formas en aparecer, no una muestra, y ahora lo dice. Con los mismos 300 valores y el mismo presupuesto, poniendo primero los largos entran 8 formas y poniendo primero los cortos entran 150: sobre una tabla ordenada lo que queda afuera es un tramo del orden. Decir cuantas quedaron sin comparar y callar cuales dejaba suponer un muestreo que no hubo.
detectar_dependencias()ganamax_trabajo, en unidades fila-par, porque ahi el costo es del orden decolumnas^2 x filasymax_comparacionesno lo veia: 158 columnas son 24.806 pares, muy por debajo de las 200.000 del tope. Se combina conmax_comparacionesy manda el mas restrictivo.perfilar()lo expone comomax_trabajo_dependencias.
Un plan que dice cuanto cuesta, no solo cuantas consultas son
plan_perfilado_dbi() contaba consultas, y contar consultas no responde la pregunta que trae quien lo mira: si la corrida tarda segundos, minutos u horas. Catorce consultas sobre dos millones de filas son mucho mas trabajo que doscientas sobre mil.
El plan estima ahora la magnitud en dos numeros que son cuentas de verdad y no un indice inventado:
filas_leidas—cuantas filas habria que leer— yordenaciones_completas—cuantas veces habria que ordenar la tabla entera—. De ahi salemagnitud:"baja","media","alta", o"desconocida"si no se conoce el numero de filas. El peso de cada clase de consulta sale de sualcance, que ya venia declarado.Al imprimirlo, un trabajo alto viene con las palancas concretas para acotarlo —
modo = "muestreado", recortarmetricas, bajarmuestra,max_consultas—. Avisar que algo es grande sin decir que hacer no le sirve a nadie.Es una estimacion y lo dice en
supuesto_costo: cuenta las filas que habria que leer si ningun indice ayudara, y cada ordenacion completa comolog2(filas)pasadas. Los dos numeros publicados no dependen de ese supuesto, asi que quien no lo comparta puede rehacer la cuenta. La referencia esta medida: PostgreSQL 16 local, 2 millones de filas por 40 columnas en modo seguro, 14 consultas y 5,3 segundos.-
Sobre la forma que se midio contra PostgreSQL 16 —2 millones de filas por 40 columnas—, la clasificacion cae donde tiene que caer, y el ultimo renglon es la razon de ser de todo esto:
modo consultas lecturas de fila ordenaciones magnitud conteos8 6.001.000 0 baja seguro14 26.001.000 0 media exacto94 186.001.000 80 alta muestreado94 2.445.000 0 baja Las mismas 94 consultas son «alta» en
exactoy «baja» enmuestreado: el conteo es identico y el trabajo difiere por setenta y seis veces. Contar consultas no podia distinguirlos. Y los conteos deconteosyseguroson los mismos 8 y 14 que se cronometraron en 2,4 y 5,3 segundos. Si aparece una clase de consulta cuyo alcance no tiene peso declarado, la magnitud queda
"desconocida"en vez de estimarse de menos en silencio.Sobre una tabla chica los parrafos de supuestos no se imprimen: tapan la respuesta en vez de matizarla. Siguen en los atributos, y la palabra «techo» viaja con el conteo en todos los casos.
Una columna que el controlador no sabe traer ya no se lleva la muestra entera
Hay columnas que muchos controladores no pueden devolver en una lectura corriente: TEXT y NTEXT en SQL Server, CLOB y BLOB en Oracle, bytea en PostgreSQL. Pedirlas junto con el resto hace fallar la consulta completa, y con ella se perdia toda la muestra. En una de las tablas de la corrida real, 90 de 158 columnas eran de esos tipos.
- Cuando la lectura de la muestra falla y hay columnas declaradas con esos tipos, se reintenta sin ellas. La muestra vuelve con las columnas que si se pudieron leer, en vez de no volver.
- Lo que quedo afuera se declara:
resumen_tabla$coberturagana una filaalcance_distintoque nombra las columnas omitidas y conserva el motivo textual del motor, mas la via para incluirlas —convertirlas a texto acotado en una vista y perfilar la vista—. - El aviso cuenta la secuencia y no atribuye la causa. El reintento salta ante cualquier fallo de lectura habiendo columnas de esos tipos declaradas; que ellas sean el motivo es lo probable, no lo comprobado, y un corte de red que se recupera en el segundo intento daria el mismo camino. Decir “el controlador las rechazo” seria informar como sabido algo que no se midio.
- El resumen por columna las cubre igual, porque esos agregados se calculan en el motor. Lo que falta es su perfil por fila, y eso es lo que dice la cobertura.
-
metatambien se corrige, y esto lo encontro la refutacion. El bloque de metadatos del muestreo se arma antes de leer, asi que quedaba congelado con la lectura que fallo:columnas_leidasdeclaraba haber leido justamente la columna que no se pudo leer, ysql_muestrapublicaba la consulta original en vez de la que de verdad se emitio. La cobertura decia la verdad ymetadecia otra cosa, que es informar como medido lo que no se midio, en el lugar donde se mira para saber que se hizo. Ahorametatrae las columnas que realmente se leyeron, el SQL del reintento, y ademascolumnas_omitidascon su motivo. - El reintento es portable: no emite conversiones propias de un motor, solo vuelve a pedir la consulta sin las columnas rechazadas. Un
CASTdistinto por dialecto habria sido otra superficie que mantener y probar contra cada motor.
Un mensaje que se leia mal
El aviso de acciones destructivas de plan_limpieza mostraba p\u00e9rdida en pantalla: la cadena tenia la barra invertida duplicada, asi que el escape nunca se resolvia. Es un error que no ve nadie —el paquete instala, la suite pasa y R CMD check no protesta, porque la cadena es ASCII perfectamente valida— y solo se nota leyendo el mensaje. Hay ahora un barrido que recorre los literales de cadena del espacio de nombres y falla si alguno lleva un escape sin resolver. Era el unico caso en el paquete.
La primera version del barrido miraba solo el cuerpo de cada funcion, y la refutacion mostro que eso dejaba fuera dos sitios donde de verdad viven mensajes: los valores por omision de los argumentos y los atributos. Ahora los recorre. Lo que sigue sin ver es una constante capturada por closure desde un ambito local, y eso queda dicho en el propio test en vez de suponerse cubierto: en lupa no es un agujero, porque las constantes del paquete son enlaces del espacio de nombres y el barrido las recorre una por una.
El costo a escala: de una consulta por columna a una por lote
La segunda corrida contra bases reales dejo una sola reserva seria, y era esta: una tabla de decenas de millones de filas no terminaba de perfilarse ni en modo = "muestreado". La causa no era el muestreo sino la cantidad de escaneos: el paquete emitia una consulta por columna para cada bloque de metricas.
Los agregados planos —conteos, minimo/maximo/media/ceros/negativos, y desvio— se piden ahora para varias columnas en una sola consulta, por lotes. La moda y la mediana siguen siendo una por columna, porque agrupan y ordenan.
-
Medido contra PostgreSQL 16 con 2 millones de filas por 40 columnas:
modo antes despues conteos46 consultas, 5,4 s 8 consultas, 2,4 s seguro128 consultas, 15,2 s 14 consultas, 5,3 s Con las mismas 160 y 400 metricas calculadas.
Y los numeros no cambian: sobre la misma tabla sembrada una sola vez, el perfil consolidado y el anterior coinciden en los dieciseis campos del resumen para seis tipos de columna, y en los noventa estados por metrica.
Si un lote falla, no se pierde el lote. Se reintenta columna por columna, y lo que igual falle queda
no_disponiblecon su motivo mientras las vecinas se calculan. Una consulta compartida es la forma perfecta de reintroducir el reflejo de todo-o-nada que el paquete corrigio en cinco lugares, asi que la degradacion se construyo desde el principio y tiene sus propios tests.resumen_tabla$sqlconserva una fila por columna y metrica con todos sus campos, y agregaloteycolumnas_compartidaspara que se vea cual consulta fue compartida.plan_perfilado_dbi()publicatamano_lote, y su total pasa a estar declarado como techo enattr(plan, "supuesto"): se contaba una mediana y un desvio por columna numerica, y una columna sin valores validos no los emite. Se afirmo durante varias rondas que el plan predecia exacto; era cierto sobre tablas con datos en todas las columnas y falso en cuanto aparece una vacia. La version anterior erraba por tres consultas en ese caso y esta por una, asi que no es una regresion: es una afirmacion que venia siendo mas fuerte que el codigo.En SQLite con tablas chicas el ahorro de tiempo es casi nulo: ahi domina el costo de R y no los escaneos. Queda dicho porque una medicion que no distingue las dos cosas invita a concluir de mas.
Lo que rompio la refutacion sobre estos mismos cambios
-
Una conversion que pierde el valor ya no se publica como
calculado. SQLite responde elMINde una columna declaradaDATEcomo el texto"2020-01-01";as.numeric()lo convierte enNAy el estado quedabacalculado. Decir “se midio” y “no se midio” a la vez sobre el mismo campo. Ahora la metrica quedano_disponiblecon el valor original del motor en el motivo. Lo mismo para uninteger64cuyo paso a doble lo cambiaria: el maximo publicado no estaria en la columna. -
La guarda de exactitud de
integer64tenia un agujero de un solo numero: comparaba el doble ya convertido contra 2^53, y 2^53+1 redondea justo a 2^53, asi que pasaba. Ahora se comprueba con la vuelta completa -a doble y de vuelta a entero-, que no depende de donde caiga el redondeo. -
La descripción de muestras independientes decía algo que la consolidación volvió falso. Las columnas de un mismo lote comparten consulta y por lo tanto comparten filas; en
muestra_motorla selección única queda además en un spool cliente y las pasadas reutilizan esa relación. -
El total de
plan_perfilado_dbi()pasa a estar declarado como techo. Se cuenta una mediana y un desvio por columna numerica, y una columna sin valores validos no los emite. La version anterior a la consolidacion erraba por tres consultas en ese caso y esta por una: no es una regresion, es una afirmacion que venia siendo mas fuerte que el codigo.
Cuatro correcciones de honestidad
Las cuatro salieron de mirar los datos crudos de la corrida real, y tres de ellas de reproducir lo que el informe atribuia a otra causa.
-
La trazabilidad acepta
integer64. Unbigintllegaba a R comointeger64y la trazabilidad lo rechazaba: el hallazgo se publicaba y la guarda tenia que avisar que no habia con que nombrar las filas. Se atribuyo a las geometrias, pero la lista incluiaoutlierssobre columnas que no tienen nada de espacial. Afecta a cualquierbigint. Por encima de 2^53 la traza no se entrega, porque la conversion deja de ser exacta y una fila mal senalada es peor que una sin senalar. -
Cuando el motor dice que es permiso, el mensaje lo dice.
dbExistsTable()no distingue una tabla inexistente de una sin permiso, y el mensaje repetia esa duda incluso cuando el motor habia respondidopermiso denegado a la relacion. En una corrida real fueron veintitres tablas descritas como inciertas con la respuesta en la mano. El texto del motor se conserva: el diagnostico no reemplaza la evidencia. -
El hallazgo
faltantesnombra la senal estructural. Cuandoposible_ausencia_estructuraldispara sobre una columna, elfaltantesde esa columna dice en su evidencia que existe esa lectura alternativa. La severidad no se toca, y eso se decidio con un caso en contra: en una tabla pivoteada la correlacion entre el mes y la columna del ano es real, y un mes sin dato puede ser un hueco genuino. Degradar ahi lo esconderia. -
El objeto declara que las metricas muestreadas no comparten filas. Estaba en la vineta, y un consumidor automatico lee el objeto. Aparece en
meta$materializacionenmuestra_motor; en los modos que miden sobre la tabla entera no hay un spool que advertir.
Leer un perfil sin conocer su forma, y saber que falta para cada motor
-
hallazgos(),columnas(),cobertura(),n_filas()ysql_perfil()leen las cuatro formas de salida del paquete sin depender de como estan armadas.perfil$general$filasfuncionaba sobre la salida en memoria y devolviaNULLsobre la salida DBI, donde el conteo vive enresumen_tabla$meta$filas; unNULLsilencioso en un guion de medicion no avisa, y lo que sigue calcula sobre nada. No inventan lo que no hay: un perfil DBI sin muestra leida devuelve una tabla de hallazgos vacia con su aviso, ysql_perfil()sobre un perfil en memoria devuelveNULLporque no se emitio SQL. -
requisitos_motor()contesta que hace falta para hablar con cada motor antes de chocarse: el paquete de R, la biblioteca del sistema con su nombre en Debian y en Fedora, la alternativa sin permisos de administrador cuando existe, el dialecto esperado y si esta probado contra motor real. Los errores de conexion se traducen: unCan't open lib ... file not foundde ODBC pasa a decir que faltaunixodbc-devy cual es la salida sinsudo. - Un controlador que no implementa
dbIsValid()ya no se toma por conexion rota: se pruebadbGetInfo()antes de rendirse. ElROraclearchivado es el caso, y por eso Oracle quedaba fuera decoleccion()aunque el SQL funcionara.
El camino de Oracle, contemplado y sin corrida que lo respalde
- Se contemplan los dos dialectos de Oracle -
fetch_firstyrownum-, que son los unicos que no comparten forma con los demas motores: los otros usanlimitotop. El camino cubre cuatro particularidades del motor: la sonda del desvio necesitaFROM DUAL; Oracle rechazaTABLESAMPLEy usaSAMPLE (p), que se agrego como forma candidata con su sonda;dbExistsTable()delROraclearchivado devuelve falso para nombres calificados aunque el SQL funcione; y una columnaCLOBno se puede agrupar ni ordenar, cosa que el paquete ya declaraba como no disponible. -
Esta entrada decia antes «Verificado contra Oracle Free 23» y «siete motores probados contra motor real». No hay con que sostenerlo: no se conserva ningun log de una corrida contra un servidor Oracle, y el unico informe que existe -del 2026-08-24- cierra diciendo que la medicion no se pudo ejecutar. El catalogo de motores da las dos variantes de Oracle como
esperado, y las pruebas de este camino corren con respuestas DBI simuladas, que es lo que se declara.
Lo que encontro una refutacion adversarial
Esta tanda se reviso al reves: buscando romper cada afirmacion en vez de confirmarla. Encontro ocho defectos, y los tres peores tenian la misma forma: la tabla con la que se verificaban los motores era comoda. Tenia 5.000 filas de tipos faciles, sin fecha nativa, sin enteros sin signo y nunca mas chica que la muestra pedida. Es el mismo error que el paquete ya persigue en los demas —el fixture que comparte la propiedad cuya ausencia es el fallo— aplicado al propio verificador.
-
Una columna
DATEse media como numero, con estadocalculado. EldbFetch(n = 0)de RMariaDB devuelvenumeric(0)para una fecha: la clase se pierde junto con las filas, eis.numeric()decia que si. Salianminimoen dias desde 1970 ymediaen YYYYMMDD -dos unidades distintas, las dos publicadas como la misma-. Ahora el tipo declarado por el motor manda sobre el prototipo. -
Un
BIGINT UNSIGNEDcerca del tope dabamaximomenor queminimo, los doscalculado. Habia guarda de coherencia para “mas distintos que validos” y no para un rango imposible. Ahora tambien. -
El muestreo extrapolaba dividiendo por las filas pedidas y no por las obtenidas. Pedir mil filas de una tabla de diez daba
n_validos = 0ysin_valoressobre una columna llena, confraccion = 1al lado contradiciendolo. El tamano que se informa y que divide es el efectivo. - El objeto declara con que criterio se comparo. Un cotejamiento que ignora la caja hace que el resumen SQL cuente dos valores distintos donde el perfil de muestra cuenta cuatro, sobre las mismas filas. Los dos numeros son ciertos en su propia comparacion; faltaba que el objeto dijera cual usa cada bloque.
-
La cobertura de una parte incompleta ya no se pierde al subir de nivel. Un conjunto armado con una organizacion a la que le falto una coleccion decia cobertura 1. Ahora hereda
cobertura_de_partesy marcacompleto = FALSE. -
El renombre de partes rompia la composicion:
agregar()escribia el nombre del objeto y el nivel de arriba comparaba contra el declarado. Los dos identifican a la misma parte. - Una parte con peso cero entraba a la cobertura sin aportar al numero, y ahora se declara.
-
posible_ausencia_estructuralno disparaba con mas de 20 niveles, o sea no veiaedad >= 65, que es el caso mas frecuente de todos. De ahi salio una capacidad nueva: un corte numerico o de fecha tambien se ofrece como regla, con la formula escrita en el tipo correcto -~ alta >= as.Date("2021-01-01"), no>= 18628-.
benchmark/verificar_motor.R incorpora ahora los tipos y tamanos que escondian esos defectos, para que la proxima tabla comoda no certifique un motor que no lo esta.
MariaDB, y las diez granularidades del marco
- Verificado contra MariaDB 11 real: los cinco modos sin ninguna metrica no disponible, los tres estadisticos coincidiendo con R, el plan exacto en los cinco. Es el primer motor real que no encontro ningun defecto, y tiene explicacion: habla el mismo protocolo que MySQL 8, que ya estaba verificado.
-
organizacion()declara que colecciones pertenecen a un organismo, y con esoagregar()mide las granularidades novena y decima del marco. Que bases pertenecen a que organismo no esta en los datos, asi que lo declara quien lo sabe; es el mismo mecanismo que ya usaba el conjunto de colecciones. -
Los dos niveles institucionales son opcionales. Un analisis de calidad no siempre tiene una organizacion detras -una entrega suelta, un archivo que alguien mando, una base sin dueno declarado- y nada obliga a pasar por ellos. Sin declaracion,
agregar()se niega y explica como declararla, que es distinto de inventar una frontera que nadie nombro. - La politica de pesos vale para los cuatro niveles con frontera: promediar organismos de tamano distinto sin declararlo es el mismo juicio inventado que el paquete se niega a hacer un piso mas abajo. Y el numero viaja con su cobertura: cuantas de las partes declaradas entraron efectivamente.
DuckDB, y una sonda que mentia
- Verificado contra DuckDB 1.5 real: los cinco modos corren sin ninguna metrica no disponible, la media, la mediana y el desvio coinciden con los calculados en R sobre la tabla entera, y los nombres calificados con punto y las colecciones de dos esquemas funcionan.
-
Y encontro un defecto que ningun motor simulado podia encontrar. DuckDB acepta
TABLESAMPLE SYSTEM (10) WHERE 1 = 0y rechaza la misma clausula sin el filtro: con un filtro trivialmente falso el parser no llega a validar el metodo de muestreo. La sonda de capacidad usaba justo ese filtro para salir barata, asi que declaraba disponible una forma que el motor despues rechazaba. Ahora la sonda emite la forma real acotada por el limite del dialecto. Una sonda que no ejercita la forma que despues se emite no prueba nada, que es la misma leccion de la sonda del desvio, una ronda antes. - El muestreo prefiere las formas de tamano predecible: primero la de cantidad fija —
TABLESAMPLE RESERVOIR (n ROWS)—, despues la de nivel de fila —TABLESAMPLE BERNOULLI (p)—, y solo al final las de bloque. Medido contra PostgreSQL 16 pidiendo el 20 % de una tabla de 5.000 filas:SYSTEMdevolvio 678, 904, 452 y 1.384 filas en cuatro corridas;BERNOULLI, 1.011, 1.017, 981 y 1.050. Un tamano que no se puede anticipar hace que dos metricas del mismo perfil dejen de ser comparables. Medido:TABLESAMPLE (20 PERCENT)en DuckDB es a nivel de bloque y devuelve0o2048filas sobre una tabla de 5.000, asi que dos consultas del mismo perfil veian muestras de tamano distinto, y la guarda de coherencia declaraba no disponible una moda cuya frecuencia superaba unos validos que valian cero. Con la forma de cantidad fija, las cuatro metricas que caian vuelven a calcularse. - El motor simulado que reproduce la trampa esta en la suite, asi que la regresion queda cubierta sin necesidad de DuckDB instalado.
La senal que faltaba: nadie declara lo que no sabe que existe
-
posible_ausencia_estructural, severidadok.aplicabilidadresolvia el vacio por diseno y funcionaba, pero exigia que el usuario supiera que existe: quien perfilaba una tabla con columnas condicionadas sin declarar nada recibia el mismo informe enganoso que antes. Ahora, cuando el valor de una columna decide que filas tienen otra —cumplimiento >= 0.99—, o cuando dos o mas columnas se reparten las filas sin pisarse, el hallazgo lo dice con la evidencia medida y la linea exacta que habria que escribir. Sugiere; no decide, y no reescribe el universo por su cuenta. Las columnas ya declaradas quedan fuera del examen. - Medido antes de encenderlo: sobre veinte conjuntos que vienen con R y sesenta tablas al azar con ausencia independiente produce cero senales, y dispara en el modelo entidad-atributo-valor, en el salto de patron de una encuesta y en las columnas excluyentes. Con 10 % de las filas fuera de la regla se calla, porque entonces la relacion existe y no es una regla. Cuesta 0,11 s sobre 200 columnas por 20.000 filas.
-
regla_silencia_ausencia, tambienok. Declarar opcional una columna con 80 % de ausentes dejaba el perfil limpio y la cobertura lo documentaba, pero quien no la leyera no se enteraba. El aviso existe para que eso sea una decision y no un efecto de la declaracion. -
columnas_personalesdeclara que columnas traen datos personales, con tipo o sin el. Ningun lexico de nombres puede ser completo —una columna con documentos se puede llamarcod_benef— y esta es la salida correcta a ese limite: lo declara quien conoce el dato, gana sobre lo inferido, y no se vuelve a examinar. -
dato_personal_protegidodice si el valor quedo protegido, no si la clasificacion pensaba protegerlo. Conproteger_datos_personales = FALSEla moda se ve, y decirTRUEal lado de un valor visible era informar como hecho algo que no paso. La intencion sigue endatos_personales$proteger. - Un correo ofuscado —
usuario at ejemplo punto com— vuelve a ser un correo para la clasificacion, aunquevalidar_correo()siga diciendo con razon que no es un correo valido. Son dos preguntas distintas: una mide la forma, la otra decide si hay dato personal. La fraselunes at casano entra: el dominio tiene que traer su separador. - Una matriz de dos dimensiones es una tabla y
perfilar()la acepta. La conversion queda declarada enmeta$entrada_convertida.
La via DBI sobre tablas que no entran en memoria
- Modos
muestreadoyaproximado. El primero muestrea en el motor —TABLESAMPLEdonde existe, orden pseudoaleatorio con limite donde no—; el segundo usa las funciones aproximadas nativas —APPROX_COUNT_DISTINCT,PERCENTILE_CONT,approx_quantile— con la misma mecanica de capacidad declarada y resuelta por sonda que ya usaba el dialecto. - Toda metrica muestreada o aproximada viaja diciendolo:
estadodistinguecalculado,estimadoyno_disponible, y cada fila llevauniverso,tamano_muestra,fraccion,metodoyerror_esperado, que esdesconocidocuando el motor no documenta una cota. Nunca una cota inventada. - El conteo de distintos tiene estado propio,
observado_muestra. La cardinalidad de una muestra no estima la del universo sin un estimador declarado, asi que se informa por lo que es, con el universo al lado. -
plan_perfilado_dbi()predice exactamente las consultas de los cinco modos. Las sondas nuevas gastan un numero fijo aunque acierten en la primera forma, por la misma razon que la del desvio: un costo que dependa del motor hace que el plan deje de predecir. - Los conteos conservan
integer64cuandobit64esta instalado, asi que un conteo por encima de 2^53 deja de perder exactitud. Sinbit64,meta$conteo_exactolo sigue declarando.
Bases enteras, y el costo de compararlas
-
detectar_relaciones()yrelaciones_coleccion()aceptancolumnas_candidatas: declarar que columnas pueden participar es lo que hace manejable un costo que crece con el producto de anchos. En una prueba de 32 por 32 columnas, 1.024 combinaciones bajan a 9. -
Dos clases de poda, y no se tratan igual. Dos columnas de la misma familia con rangos numericos disjuntos no comparten ningun valor y eso se sabe sin comparar: la fila sale como siempre y la comparacion se ahorra. Esa poda esta siempre activa porque no cambia nada de lo informado. Las otras dos —familias distintas, cardinalidad imposible— si lo cambiarian: 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 sino que no llegan al umbral. Van detras depodar = TRUE, y cuando se aplican el par no desaparece: sale concardinalidad = "sin_comparar", coberturasNAy su motivo. Un par que no se evaluo no es un par sin relacion. -
tope_memoria_mbacota las filas comparadas y declara los pares pendientes en vez de devolver menos sin decirlo. - La granularidad
conjuntoColeccionespasa a medirse, con la frontera declarada por el usuario y pesos explicitos: agregar entre colecciones sin pesos seria inventar un juicio, que es lo queindice_calidad()se niega a hacer.organizacionyconjuntoOrganizacionessiguen sin implementar, y por la misma razon de siempre: no falta codigo, falta el objeto. - La entrada
data.framedecoleccion()validaNA, cadenas vacias y tipos igual que la entrada por vector de texto. Dos puertas del mismo paquete dejaron de comportarse distinto ante la misma entrada mala.
Lo que el detector de vocabulario no puede ver, dicho
-
n_grupos_sin_variante_raracuenta los grupos de formas cercanas que el criterio de variante rara nunca llego a formar. Una variante mal escrita que ocupa la mitad de la columna no es una variante rara para el comparador y no se informaba; ahora el limite se declara aunque la deteccion no cambie. -
variantes_equifrecuentes_vocabularioes el diagnostico para ese caso: dos formas cercanas que se reparten la columna sin que ninguna sea dominante, que es la firma de dos operadores, una plantilla rota o una migracion parcial. Queda apagado por omision, y la razon esta medida: sobre la bateria de 31 tablas limpias produce un grupo sospechoso donde no hay defecto, y dispara en tablas de menos de veinte filas. Es aditivo: encenderlo no cambia ni pierde ninguna deteccion decasi_duplicados_vocabulario. - La evidencia de
patron_rarodeclaradesvio_unicamente_largo_corrida_numericacuando el unico desvio es la cantidad de digitos. No baja el ruido de trescientos correos correlativos —eso no tiene solucion sin dominio— pero convierte una lectura de dos segundos en una de cero. - La razon de permutacion viaja como evidencia descriptiva del detector de orden. No filtra nada: el criterio quedo refutado con precision 0 % en cuatro tablas reales y no se usa para decidir.
Costos declarados donde antes solo se tardaba
-
max_comparaciones_dependenciasacota la busqueda de dependencias funcionales, cuyo costo es del orden decolumnas^2 x filasy empeora con determinantes casi unicos. Cuando el presupuesto se agota, lo comparado se informa y lo que quedo sin comparar se declara. - La deteccion de fechas partidas dejo de materializar el producto cartesiano de los candidatos ano/mes/dia; el detector de vocabulario dejo de recorrer el vocabulario completo antes de aplicar su tope. Los dos declaran lo que no evaluaron.
- La confirmacion de un validador de documentos deja de recorrer la columna entera sin presupuesto. Cuando el tope se alcanza, el fundamento dice sobre cuantos valores se confirmo.
-
datos[0, 0]sobre un objetosfconserva la geometria, porque esa columna es pegajosa por diseno de ese paquete. Con las dependencias apagadas, el objeto vacio llegaba con una columna y el diagnostico declaraba un recorte que nadie pidio. - Un par no comparado trae cobertura
NA, ydatos[NA, ]devuelve una fila entera deNAen vez de ninguna.relaciones_coleccion()filtra conwhich().
Infraestructura
-
inst/WORDLISTcompleta la lista que faltaba:spelling::spell_check_package()vuelve cero. Las palabras son nombres propios, siglas, terminos tecnicos y fragmentos de identificadores del paquete. -
CONTRIBUTING.mdcorrige el orden de la verificacion previa.test_dir()ytest_file()cargan el paquete conlibrary(lupa), que no expone las funciones internas y produce veinte errores falsos de «could not find function»;test_check("lupa")es lo que correR CMD check, y necesita el paquete instalado.
Cuatro motores reales
- El desvio se pide primero con la funcion nativa del motor —
STDDEV_SAMPdel estandar,STDEVen SQL Server— y solo cae al calculo de dos pasadas donde no existe ninguna de las dos. La forma anterior ponia la media como subconsulta escalar para no incrustarla como literal en el SQL guardado, y SQL Server rechaza una subconsulta dentro de un agregado: el arreglo de privacidad habia roto la compatibilidad, y solo un motor real podia mostrarlo. - Verificado contra PostgreSQL 16, MySQL 8, SQL Server 2022 y SQLite: en los cuatro, ninguna metrica queda no disponible, y la media, la mediana y el desvio calculados por el motor coinciden con los calculados en R sobre la tabla entera. En SQL Server la sonda resuelve el dialecto
toppor su cuenta.
Numeros que no pueden ser
perfilar_dbi()resuelve un nombre calificado con punto igual quecoleccion().dbExistsTable()no lo resuelve, asi que el mismo texto funcionaba en una funcion y fallaba en la otra diciendo que la tabla no existe. Un nombre literal con punto adentro sigue teniendo prioridad.El universo aplicable declarado sale tambien del analisis, no solo de los conteos. Con filas no aplicables que tienen valor,
n_distintoslas contaba mientrasn_validosya no, ytasa_distintospodia pasar de 1.La trazabilidad no nombra filas fuera del universo declarado. El conteo ya las excluia y nombrarlas igual producia la incoherencia que la guarda detecta.
La via DBI valida la coherencia interna de lo que informa el motor: mas valores distintos que validos, o una frecuencia de moda mayor que las filas validas, son imposibles y se declaran no disponibles en vez de publicarse como calculados.
El lexico de nombres de columna con datos personales cubre
persona,cliente,paciente,socio,beneficiario,titular,funcionario,usuario,solicitante,responsable,contribuyente,residencia,lugar_residenciaybarrio. Ningun lexico puede ser completo; estos son los frecuentes en registros administrativos.
La via DBI deja de asumir un dialecto y de tirar lo que ya midio
-
perfilar_dbi()resuelve el dialecto con una sonda de cero filas antes de emitir el bloque de agregados:limit,top,fetch_first,rownumy una via portable condbSendQuery()+dbFetch(n). Se puede declarar condialecto =si la sonda no acierta. - Las cuatro consultas obligatorias —campos, conteo, esquema y muestra— dejaron de ser fatales. Si la muestra falla, el objeto vuelve con
resumen_tablacompleto,perfil_muestra = NULLy una fila de cobertura con el motivo. Antes, un motor que no aceptaLIMITdescartaba las 777 consultas ya pagadas. - El esquema y la muestra enumeran columnas en vez de usar
SELECT *, y si la lectura conjunta falla sondean columna por columna para descartar solo la que el motor rechaza. - Los
stop()de la via DBI tienen clase de condicion propia, asi que un fallo se puede atrapar y el resumen rescatar. - Los alias se comillan y se comparan sin distinguir caja. Un motor que los pliega a mayusculas ya no produce metricas con estado
calculadoy valor vacio, que era peor que declararlas no disponibles. - Argumentos nuevos para acotar el costo:
modo,metricasymax_consultas, masplan_perfilado_dbi(), que dice cuantas consultas va a costar el perfilado antes de emitirlas. -
resumen_tablapasa por la proteccion de datos personales, que antes solo alcanzaba al perfil de la muestra: el bloque sin proteger era justamente el de alcance completo. El SQL guardado del desvio ya no incrusta la media observada. Nuevoprint.perfil_dbi, que no imprime ningun valor de celda.
El nivel coleccion deja de informar cero donde no midio
- Una tabla vacia ya no produce
prop_faltantes_maxima = -Infnin_columnas_sin_faltantes = 0: sonNA, con una fila de cobertura que declara que no hay nada que medir. Antes esa tabla se ordenaba como la de mejor calidad de la base. - Componente nuevo
cobertura_metricas: la declaracion de lo que el motor rechazo sube al nivel coleccion antes de descartar los perfiles, y existe tambien conconservar_perfiles = FALSE. -
estimar_costo_coleccion()usa la formula cerrada en vez de materializar los pares: 27,4 s y 233 MB con 1700 tablas pasaron a 0,42 s. El resultado es identico. Acepta cero pares, deduplica y rechaza los autorreferenciales. -
relaciones_coleccion()cachea cada tabla en vez de releerla una vez por par. - Los identificadores de mas de dos partes se rechazan nombrando la causa real. Antes se aceptaban y el fallo se le devolvia al usuario como un problema de permisos sobre una tabla que si podia leer.
El perfilado espacial deja de ser inviable por tiempo
- Las columnas no atomicas ya no pasan por la maquinaria de texto. Convertirlas no producia sus valores sino su representacion como codigo, una vez por cada etapa que las tocaba: era el 85 % del costo de perfilar una capa espacial. Perfilar 62 poligonos de 200.000 vertices paso de 323 s a 2,3 s.
- La transformacion de coordenadas se hace en una sola llamada y no una por geometria.
- Presupuesto de geometrias y de vertices, con el recorte declarado.
- WKT, WKB y hexadecimal se detectan, se convierten y se miden. Antes quedaban todas las metricas en
NAconcobertura_diagnosticosvacia, ycobertura_analisis()llegaba a afirmar que la geometria no aplicaba sobre datos que si eran geometricos. - Un fallo parcial ya no descarta la columna entera: una geometria intransformable o un
NAde la validez dejan de borrar el conteo y los indices de todas las demas.
El vacio por diseno se declara y deja de contarse como defecto
-
perfilar()aceptaaplicabilidad, una lista de formulas por columna que declara en que filas la columna corresponde. Las filas fuera del universo salen den_faltantesy deprop_faltantesen vez de contarse como ausencia. Antes, una tabla completa en las filas donde el dato corresponde podia informar completitud baja: el conteo era correcto y la lectura falsa. -
perfilar()aceptacolumnas_opcionalespara el caso mas simple, donde la ausencia nunca es defecto y no hay una regla que escribir. - La regla declarada, el universo resultante y las filas donde la regla no se pudo determinar quedan en
cobertura_diagnosticos. Un universo recortado sin constancia seria el mismo defecto al reves. - Las filas donde la regla no se puede evaluar no se cuentan como aplicables ni como no aplicables: van a
n_aplicabilidad_indeterminada, porque no saber no es lo mismo que no corresponder. - Nuevo hallazgo
valor_fuera_de_aplicabilidad: un valor presente donde la regla dice que la columna no corresponde. Es el error simetrico y sin universo declarado no tenia forma de aparecer. - La metrica
NoNuloaceptaaplicablecon el mismo criterio, para que el universo declarado llegue al tablero y no solo al hallazgo. - Nueva funcion
perfilar_por(): perfila cada grupo de filas por separado y devuelve los hallazgos de todos los grupos en una tabla. Es la respuesta al formato largo, donde una sola columna mezcla dominios sin relacion. Las columnas enteramente ausentes dentro de cada grupo se descartan antes de perfilar, y el descarte se declara. - Nueva vineta
vacio-por-diseno, que documenta el supuesto tabular del paquete y las seis formas de tabla donde no vale.
Privacidad: ante la duda se protege
- El clasificador de datos personales dejaba sin proteger una columna cuya forma era compatible con un documento de identidad cuando el validador no podia verificarla. Ese es justamente el caso de una base sucia, y los valores reales terminaban escritos en la evidencia de los hallazgos. Ahora se protege igual; la evidencia sigue declarando que la clasificacion es debil.
Conteos que no se pueden contar
-
.moda_columna()distinguia mal dos ausencias: la frecuencia cero de una columna sin valores validos y la imposibilidad de contar sobre una columna no atomica. La segunda ahora esNA. - El hallazgo
constantesobre una columna no atomica informa la frecuencia que se deduce de las filas validas y las nombra en la trazabilidad, en vez de informar cero afectados. El discriminador dejo de ser la etiqueta del tipo, que dejaba afuera a las columnas espaciales.
Recortes declarados donde se los busca
- El recorte por
max_columnas_dependenciasse declara encobertura_diagnosticos, como ya lo hacia el recorte hermano de la busqueda aritmetica. El tope aplicado se conserva como atributo, y el motivo aclara que la seleccion de columnas es por posicion.
Patrones raros: ventana de operacion visible
-
patron_rarodeclara encobertura_diagnosticoscuando no puede ejecutarse porque el patron dominante no alcanzaumbral_patron_dominante. La fila conserva la proporcion observada y explica como ajustar ese argumento. - La evidencia de cada hallazgo
patron_raropublica la proporcion del patron dominante y cuantas filas quedaron en patrones no dominantes excluidos por superarumbral_patron_raro. Ese conteo queda en la evidencia, no en la cobertura, porque no es una no medicion del diagnostico.
Patrones raros y trazas accionables
-
patron_raroconserva separado el tope de presentación y el alcance de la trazabilidad:resumen_patronesy la evidencia siguen mostrando como máximo seis patrones, mientras la traza usa sus nombres raros completos hasta un límite de 5.000. Si se alcanza ese límite,cobertura_diagnosticosy el alcance de la traza lo declaran. - Las ausencias de una columna de lista se nombran, no sólo se cuentan. Una columna de listas —o un BLOB leído por
perfilar_dbi()— informaba cuántos valores faltaban sin decir en qué filas, aunqueis.na()los identifica elemento a elemento y es el mismo criterio con el que se contaron. Lo encontró la propia guarda de coherencia, que era exactamente para lo que se agregó. -
casi_duplicados_vocabularioentrega primero las filas de formas no dominantes y después las de formas dominantes. La unidad sigue siendovalor_distinto, el grupo sigue incluyendo la forma dominante y la evidencia informa cuántas filas mostradas pertenecen a cada tipo de forma.
La traza de vocabulario y la guarda de coherencia cierran el circuito
-
casi_duplicados_vocabularioconservaunidad_conteo = "valor_distinto"y ahora enumera las filas que contienen los valores de cada grupo seleccionado, incluida la forma dominante. La distancia sigue siendo una señal heurística, no una afirmación de identidad. -
perfilar()conserva cualquier hallazgo cuya traza no coincida y emite la advertencia de claselupa_trazabilidad_incoherente. La guarda compara el total anterior al límite de presentación, funciona en ambas direcciones y adapta la comparación a la unidad declarada.
Conteos y trazabilidad dejan de mezclar unidades
-
mayusculas_inconsistentesynormalizacion_unicodedeclaranunidad_conteo = "valor_distinto"y cuentan enn_evaluadoslos valores distintos evaluados.n_afectadosya contaba esos valores; su traza sigue siendo por fila y enumera todas las filas que contienen los valores afectados, no sólo las defectuosas. -
filas_duplicadascuenta ahora todas las filas participantes de los grupos, en línea conEntidadDuplicaday conmarcar_filas_duplicadas. La evidencia conserva el número de excedentes para la acción que elimina repeticiones. - Una constante de listas cuya frecuencia no puede contarse informa
NAenn_afectadosy deja el motivo encobertura_diagnosticos; una matriz no analizada enumera todas sus filas en la trazabilidad.
La trazabilidad deja de recalcular lo que el detector ya decidió
Un hallazgo dice cuántas unidades afecta y, cuando puede, cuáles. Ese «cuáles» lo resolvía una rama de índices aparte que en varios casos aplicaba un criterio distinto del detector que había producido el hallazgo. Los dos no coincidían, y el desacuerdo no se veía porque nada comparaba la evidencia contra los índices.
-
patron_rarono nombraba ninguna fila cuando la columna tenía algún patrón de frecuencia intermedia. La guarda comparaba el total de patrones distintos contra el tamaño del resumen, que son cosas distintas: el resumen es el patrón dominante más los patrones raros, no un top-N, así que se disparaba en una situación perfectamente normal. Ahoradescubrir_patrones()exponen_patrones_rarosy la guarda pregunta lo único que corresponde —si ese conjunto fue recortado por el tope de seis—. Sin recorte la enumeración es completa yn_afectadostoma su valor real; con recorte se enumera igual y el alcance espatrones_parciales, declarado en la cobertura. -
outliers,valores_no_finitos,ceros_no_permitidosynegativos_no_permitidoscondicionaban la enumeración a que la columna fuera numérica en su tipo declarado. Al leer un CSV como texto —el caso más común que hay— el perfilador infiere numérico, convierte y cuenta bien, pero la rama miraba uncharactery no devolvía nada: se informaban diez atípicos y no se nombraba ninguna fila. Ahora rastrean sobre la vista cuantitativa inferida, la misma que usó el detector. -
codificacion_rotareimplementaba la detección con una clase de caracteres más angosta que la del detector, de modo que un valor con el mojibake del carácter de reemplazo se contaba y no se nombraba —y ese vacío salía declarado con alcancecompleto, que es justo lo que este paquete no hace—. Ahora reutiliza la máscara del detector. -
patron_raronombraba, además, filas que su propia evidencia acababa de descartar. En una secuencia entera densa, un patrón que difiere sólo por el largo —9frente a9+— no es un desvío: es el mismo número con menos dígitos. El detector lo filtraba al armar la evidencia; la rama de índices yn_afectadosrecorrían el resumen crudo. El conjunto filtrado se calcula ahora una sola vez y viaja con el resultado, de modo que no puede haber dos criterios.
El principio que unifica los cuatro: la trazabilidad no recalcula lo que el detector ya resolvió. Cada vez que lo recalculaba, los dos criterios se separaban en silencio.
Y la prueba que faltaba. La suite verificaba conteos, no identidades: una prueba que comprueba n_afectados == 10 pasa igual si el paquete nombra diez filas equivocadas, ninguna, o seiscientas. Por eso ninguno de estos desajustes se veía con toda la suite en verde. Ahora hay fixtures que construyen tablas con índices corrompidos conocidos de antemano y verifican aciertos, falsos positivos y pérdidas.
El piso de asimetría del vocabulario declara lo que deja afuera
- El comparador de vocabulario abría grupos por distancia con cualquier desbalance de frecuencias, y así señalaba
estefrente aoeste—dos puntos cardinales— como posibles variantes de un mismo valor. Medido sobre tablas limpias y sobre erratas sembradas, los falsos positivos quedan entre1,0y1,5de asimetría y las erratas reales desde9,0, así que ahora se exige una asimetría mínima de2, configurable conmin_asimetria_vocabulario. -
El piso no se aplica a los grupos formados por normalización.
Montevideo,MONTEVIDEOyMontevideoson tres grafías del mismo valor y con una aparición cada una su asimetría es1,0: ahí la equivalencia está comprobada y no es una conjetura sobre una errata. -
Y lo que el piso deja afuera se declara. En la banda de asimetría baja cae también una errata sistemática que afecte a una fracción grande de los registros, y por la forma es indistinguible de dos valores legítimamente parecidos. Elegir en silencio cuál se sacrifica sería justo lo que este paquete no hace:
cobertura_diagnosticosinforma cuántos grupos quedaron bajo el piso y cómo bajarlo.
Colecciones: el séptimo nivel de granularidad deja de estar sólo declarado
-
coleccion()declara qué tablas componen una base de datos, con su esquema, yperfilar_coleccion()devuelve una fila por tabla con sus agregados exactos, máscobertura_coleccioncon lo que no se pudo medir. La granularidadcolecciondel marco estaba declarada y no se medía: lo que faltaba no era código sino el objeto. - La frontera se declara, nunca se descubre. Recorrer el catálogo convertiría un error de permisos en un resultado, y una colección real pasa de mil tablas repartidas en decenas de esquemas.
- El esquema es parte de la identidad de la tabla, así que el mismo nombre en dos esquemas son dos tablas y no una repetida.
- Lo que no se pudo leer se declara y nunca queda en cero: una tabla sin permiso, un objeto declarado como vista, un motor que rechaza un agregado. En bases institucionales los permisos parciales son el caso normal.
- Cada tabla declara su propio muestreo, y no se promedian alcances distintos como si fueran uno.
-
No hay lectura instantánea. Perfilar una colección son muchas consultas y la base puede cambiar entre ellas, así que cada fila trae el
momentoen que se midió ymeta$snapshotdeclara que no lo hubo. - El perfil pesado de cada tabla no se retiene salvo que se pida con
conservar_perfiles = TRUE: con cientos de tablas no entraría en memoria. -
agregar()mide ahora esta granularidad, con tres condiciones. Exige la frontera declarada, porque sin saber sobre qué tablas se agrega el número no significa nada. Admite sólopromedio_ponderado: sin esa restricción bastaba pedirpromediopara obtener un número entre tablas de universos distintos sin declarar nada, que es el juicio que el paquete se niega a inventar. Y la cobertura viaja pegada al número. - Esa última condición es la que más importa, y salió de refutar el diseño. Un número sobre «la colección» calculado sólo con las tablas que se pudieron medir informa como medido lo que no se midió: el peso de la tabla ausente desaparece en vez de manifestar la falta de cobertura. Con quince tablas declaradas y seis sin permiso, el número describe nueve y se presenta como si describiera la colección. Ahora el resultado trae
tablas_declaradas,tablas_en_el_numero,tablas_sin_medircon su motivo, lacoberturay la advertencia de que leerlo sin ella sería exactamente ese error. -
relaciones_coleccion()busca claves foráneas candidatas entre los pares que se declaren, yestimar_costo_coleccion()permite ver el costo antes. Los pares se declaran por la misma razón que la frontera: una clave foránea es dirigida, así que mil tablas dan casi un millón de direcciones, y el costo real no lo da el número de tablas sino el de comparaciones entre columnas. Cada par se compara sobre una muestra, y el objeto declara que 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. Un par que no se pudo leer se declara encobertura_paresen vez de desaparecer. -
granularidades()declara el séptimo nivel como implementado: siete de diez. Los tres últimos siguen sin objeto, y no por falta de código: qué bases componen un conjunto y qué bases pertenecen a un organismo son decisiones de gobernanza que no están en ningún dato.
Evaluar estimaciones que calculó otra herramienta
-
medicion_desde_estimaciones()recibe estimaciones ya calculadas —porsurvey, porcalidaddel INE de Chile, o por cualquier otra fuente— y las lleva al contrato demedir(), para poder evaluarlas contra un marco declarado.lupano estima: eso necesita diseño muestral, estimación de varianza y otra disciplina; lo que sabe hacer es evaluar contra un marco, y eso es lo que ofrece. - Cada estadístico se convierte en su propia medida canónica, con su métrica, su tipo, su unidad y su orientación, porque los siete tienen dominios distintos: un coeficiente de variación de
0,30y un tamaño de muestra de0,30no se leen igual.estadisticos_estimacion()publica el catálogo. -
La procedencia viaja en cada medida y es obligatoria, para que nadie lea el resultado como si
lupalo hubiera calculado. Los estadísticos que la tabla no traiga no se rellenan con ceros: se declaran ausentes.
Señales redundantes: la contradicción que ninguna columna muestra sola
-
senal_redundante()declara que varias columnas de una tabla codifican el mismo hecho, ydetectar_discordancias()informa las filas donde no concuerdan dentro de la ventana declarada. El caso típico son el año de la fecha, el año fiscal y el año del archivo: los tres pueden ser plausibles por separado y aun así contradecirse. - El grupo se declara, nunca se adivina. Dos columnas de año pueden ser el de nacimiento y el de ingreso, y no tienen por qué coincidir; suponerlo sería inventar conocimiento del dominio.
-
transformacionlleva columnas guardadas de formas distintas a una escala comparable —extraer el año de una fecha, por ejemplo—, yventanaes la tolerancia en las unidades del valor comparado, que no se adivina. - Una fila con alguna columna ausente no cuenta como desacuerdo: sale del universo, y
n_evaluadaslo declara. Si ninguna fila tiene todas las columnas presentes,n_discordantesqueda enNAy la señal se declara no evaluada, en vez de informar cero discordancias.
Los umbrales de una regla salen del closure y se pueden consultar
-
regla_evaluacion()aceptaumbrales, una lista con nombres que se le pasan a la condición al evaluarla. Antes el umbral quedaba encerrado en el closure: para mover un número había que escribir otra regla, y nadie podía consultar cuál era. Ahora la misma función evalúa distinto con dos umbrales —0,67 y 0,33 sobre los mismos valores— sin reconstruir la lógica. - Una condición que no recibe un umbral declarado se rechaza enumerando los argumentos que sí acepta, en vez de ignorarlo en silencio. Una condición con
...los recibe todos. -
propiedades_regla()muestra lo que una regla declara: métricas, nivel, proporción mínima, desenlace y umbrales. Es la contraparte depropiedades_metrica(), que describe métricas: un umbral pertenece a una regla y no cabía allí.
Trazabilidad por clave declarada: del hallazgo que se lee al que se verifica
-
perfilar()aceptaclavecon las columnas que identifican una fila. La trazabilidad de cada hallazgo trae además el valor de esas columnas para las filas señaladas, así que el caso se puede buscar en el sistema de origen sin abrir la tabla.analizar()lo traslada por...y porargumentos_perfil. - La trazabilidad separa dos ejes que antes se confundían:
estado—disponible,truncada,no_disponible,no_aplica— dice si se pudo localizar y hasta dónde, ylocalizador—indice_fila,clave_declarada,ninguno— dice con qué. Una trazabilidad puede ser al mismo tiempo por clave y truncada. - Las claves viajan como data frame, una fila por índice mostrado y una columna por componente: concatenarlas perdería los tipos y haría ambigua una clave compuesta.
-
La clave que permite verificar es la que identifica a una persona. Si alguna de sus columnas se clasifica como dato personal y la protección está activa, sus valores salen enmascarados igual que la evidencia, en todos los hallazgos —la clave viaja con la fila, no con la columna del hallazgo— y
claves_protegidasdeclara cuáles se enmascararon. - Una clave que nombra columnas inexistentes se rechaza enumerando las disponibles; una que no es única avisa y sigue, porque sirve igual para localizar aunque deje de ser una clave.
La cobertura del vocabulario deja de contradecir al hallazgo
-
casi_duplicados_vocabularionombraba dos diagnósticos distintos: agrupar valores por su forma normalizada, que no depende de nada, y medir proximidad por distancia de edición, que necesitastringdist. Sin ese paquete el primero medía y el segundo se declaraba bajo el mismo nombre y para la misma columna, así que cruzarcobertura_diagnosticosconhallazgospor(diagnostico, columna)—el uso natural para un consumidor automático— devolvía una contradicción: el mismo diagnóstico declarado como no evaluado y reportado como medido. - La cobertura pasa a llamarse
proximidad_vocabularioen las tres razones que le corresponden: faltastringdist, el vocabulario excede el alcance de comparación, y el grupo candidato mayor abarca tanto que el diagnóstico no aplica. El hallazgo conserva su nombre. Quien filtre la cobertura por el nombre viejo tiene que actualizar el filtro.
Dos cosas más que el objeto ahora declara
-
patron_rarodistingue en la evidencia las dos clases de desvío:clase_desvio=largo_de_corridacuando el valor señalado sigue el mismo patrón con un número de otro largo —persona9@frente apersona300@— yclase_desvio=estructuralcuando es otra forma —SIN CODIGOfrente aAB-12345—. La severidad no cambia: los dos casos son indistinguibles por la forma y eso está medido. Lo que cambia es que quien lee el hallazgo lo resuelve de un vistazo en vez de comparar patrones a ojo. -
agregar()acepta el nombre relacional de la granularidad —celda,columna,tupla,tabla— igual quemetrica(). Su propio mensaje de error ya los enumeraba, así que rechazarlos era una inconsistencia. El objeto sigue guardando el nombre canónico del marco.
Spearman para relaciones monótonas que no son lineales
-
detectar_asociaciones()aceptametodo_numerico = "spearman"y mide asociación monótona sobre los rangos, sin suponer linealidad. Sobre una relación cúbica con ruido, Pearson da 0,918 y Spearman 0,997. Pearson sigue siendo el valor por omisión, y el método elegido viaja en la columnametodocon su supuesto ensupuesto, así que ninguna lectura depende de recordar cuál se pidió.analizar()lo traslada conmetodo_asociacion_numerica. - Los dos README explican ahora dónde viven la distribución de valores y las correlaciones —en
analizar(), no enperfilar()— y por qué esa separación es deliberada.
Tres afirmaciones que el paquete hacía sin fundamento suficiente
-
alta_cardinalidadse apoyaba sólo en la tasa de valores distintos, y con pocas filas esa tasa está dominada por el tamaño: una columna de dos valores en tres filas daba 0,67 y superaba el umbral, aunque una columna de dos valores no puede tener cardinalidad alta. Ahora el hallazgo exige además al menos diez valores distintos. Las columnas con cardinalidad alta real —treinta valores distintos en cuarenta filas— se siguen informando igual. -
columnas_duplicadasafirmaba que dos columnas tienen el mismo contenido en tablas sin ninguna fila, donde dos columnas vacías coinciden sin que eso sea evidencia. Ese caso pasó acobertura_diagnosticoscon su motivo. Cuando sí hay filas, la evidencia declara ahora sobre cuántas se comparó. -
relacion_aritmetica_columnasse salteaba en silencio cuando la tabla no llegaba al mínimo de filas comparables. Ahora se declara encobertura_diagnosticos, y sólo cuando había combinaciones de columnas numéricas que evaluar.
El vínculo entre una acción del plan y su hallazgo ya no depende de la prosa
-
planificar_limpieza()recuperaba el par de columnas duplicadas comparando la cadena de evidencia completa del hallazgo. Al enriquecerse ese texto, dos pares distintos colapsaban en el mismo y el plan perdía una acción. El vínculo se hace ahora contra el primer tramo de la evidencia, que es el que identifica el par.
El perfilado no toca los datos, y ahora está probado
- Ninguna función de análisis altera la tabla que recibe: ni sus valores, ni sus tipos, ni sus nombres, ni sus atributos. Una prueba de regresión lo verifica en
perfilar(),analizar(),detectar_claves(),detectar_dependencias(),distribucion_valores(),detectar_asociaciones(),detectar_duplicados_aproximados(),planificar_limpieza()yguiar_limpieza(). El caso que importa esdata.table, que R permite modificar por referencia: la prueba compara además la dirección de memoria del objeto.aplicar()devuelve una copia y deja intacta la original.
Duplicados: el hallazgo no afirma una igualdad que produjo la normalización
- La normalización por omisión iguala mayúsculas, espacios, acentos y comillas, así que un par puede coincidir después de normalizar sin que los valores guardados sean iguales.
tipo_pardistingue ahoraexacto,exacto_normalizadoyaproximado;igualo_normalizardeja esa causa visible en cada fila. El hallazgoduplicados_exactos_normalizadosevita afirmar que dos filas tienen los mismos valores y la trazabilidad lo busca entre los pares de ese tipo.n_pares_exactoscuenta sólo texto guardado igual yn_pares_exactos_normalizadoscompleta la explicación junto conn_pares_aproximados.
Casi-claves y precedencia de ausencias
-
perfilar()informa unacasi_clavecuando una columna tiene al menos 100 filas, supera 90 % de valores distintos y al menos la mitad de sus duplicados excedentes se concentra en un valor. Las fechas y fecha-hora se excluyen por su rol propuesto. La evidencia enumera las colisiones, sus frecuencias y los criterios aplicados. Los vectoresdoublecon algún valor finito fraccionario se excluyen, mientras que los formados por valores enteros se conservan para admitir identificadores importados desde archivos de texto. Los vectoresinteger64cuentan como enteros semánticos.detectar_claves()las expone sin confundirlas con claves exactas, yanalizar()las reitera en sus advertencias. -
casi_duplicados_vocabularioretira primero los valores ya detectados comofaltantes_disfrazados. Un centinela de ausencia deja de presentarse como posible errata de otro valor; las variantes que no son centinelas conservan el diagnóstico.
Tablero, indice declarado y medicion agregada
-
tablero_calidad()resume una corrida por metrica y objeto, declara la agregacion aplicada en cada fila y conserva el alcance completo del marco. -
indice_calidad()no calcula nada sin pesos del usuario. Con una declaracion completa conserva cobertura, pesos por dimension, combinaciones internas, inversiones de defectos, exclusionesno_aplicay la advertencia de que los componentes provienen de universos distintos. -
analizar()mide por omision la propuesta en estadolista, declara que no fue confirmada, agrega las medidas y conserva el tablero. El detalle fila a fila solo se retiene conconservar_detalle_medicion = TRUE; la medicion automatica se desactiva conmedir_propuesta = FALSE.
Secuencias enteras densas y vocabularios breves
- El perfil de columna publica si los enteros observados cubren densamente su rango, junto con densidad, posiciones y huecos. En esa condicion los centinelas numericos y los desvios que solo expresan el largo de una corrida de digitos no interpretan el contenido del identificador; los ausentes, duplicados y restantes diagnosticos siguen activos. Una secuencia densa y unica se presenta como
posible_identificadory no recomienda convertir el texto numerico a una medida cuantitativa. -
casi_duplicados_vocabulariocubre una sustitucion en valores de hasta seis caracteres cuando la variante ocupa como maximo0.05de la columna y la forma dominante es al menos10veces mas frecuente y ocupa al menos0.5de la columna. El limite y los tres umbrales quedan en la evidencia y se pueden ajustar enperfilar().
Orientacion explicita de las metricas
-
metrica()declara si un resultado expresa"conformidad","defecto"o"no_aplica". La orientacion viaja pormedir(),agregar(),reportar()yevaluar()sin invertir los valores; una regla puede recibirla como segundo argumento. El historico conserva el esquema 1 y sigue leyendo archivos anteriores. -
Formatoqueda alineada con el factorCorrectitud sintacticademarco_agesic(), y una prueba contrasta todos los pares dimension-factor del nucleo contra el marco.
Marco CEA/CEPAL de aseguramiento de la calidad
-
marco_cepal()incorpora los cuatro niveles y diecinueve principios del marco nacional de aseguramiento de la calidad de las Naciones Unidas, adoptado y adaptado para América Latina y el Caribe por la CEA/CEPAL. Los principios 1 a 13 quedan declarados fuera del alcance de una tabla; los principios 14 a 19 quedan disponibles para documentar productos estadísticos, sin afirmar que el profiling genérico los mida.
Severidad del vocabulario y escala de las relaciones
-
casi_duplicados_vocabularioqueda como señalsospechososólo cuando encuentra grupos; un resultado negativo queda comookcon cero afectados, y un diagnóstico que no aplica se registra encobertura_diagnosticos. -
relacion_orden_columnassepara la escala de la relación fila a fila con un solapamiento intercuartil mínimo de0.1. Una brecha con IQR cero conserva una relación estable aunque los rangos no se solapen; ambos criterios y los pares descartados o recuperados quedan en el alcance.
Perfil de una muestra DBI con universo explícito
- Se agrega
perfilar_dbi()para separar los agregados SQL exactos sobre una tabla completa del perfil de 99 campos calculado sobre una muestra declarada. La salida registra el motor informado por DBI, cada consulta, los agregados no disponibles y la reproducibilidad efectiva del orden, sin escribir en la base.
Desenlaces declarados por reglas
-
regla_evaluacion()aceptadesenlace = "suprimir"para que una regla declarada por el usuario produzca un plan sobre las medidas que no cumplen su condición. La evaluación conserva objeto, valor medido, motivo y regla sin modificar la medición ni los datos de origen. Sin esa declaración no crea desenlaces ni aplica umbrales de publicación. -
reportar()enmascara los valores alcanzados por ese plan tanto en la evaluación como en las mediciones incluidas en el mismo reporte. El enmascarado se hace sobre copias usadas para renderizar.
Ley de Benford con aplicabilidad explícita
-
perfilar()evalúa la ley de Benford solamente en columnas numéricas con suficiente evidencia inicial. Antes de comparar exige variación, ausencia de apariencia de identificador (incluidas secuencias correlativas), al menos 100 valores positivos, todos los valores finitos positivos y tres órdenes de magnitud. Las precondiciones y sus umbrales quedan enmeta$benford; las que fallan se declaran encobertura_diagnosticosy no producen hallazgos. - Cuando aplica, el perfil conserva la distribución observada y esperada por primer dígito, el chi-cuadrado de Pearson y su valor p. Una desviación se presenta como señal descriptiva para revisar, nunca como acusación de fraude o manipulación.
URLs, unidades y celdas multivaluadas
-
validar_url()valida de forma vectorizada URLshttpyhttps, con esquema obligatorio por omisión, soporte para IDN y puertos, y rechazo deliberado dejavascript:,data:, espacios y controles literales. -
perfilar()informaunidades_mixtascuando una columna numérica escrita como texto combina sufijos de unidad, conservando sus frecuencias y sin convertir datos. Reconoce además monedas como prefijos o sufijos y emitemonedas_mixtascon sus frecuencias, sin convertir ni suponer tasas de cambio. También informaceldas_multivaluadassólo cuando las partes homogéneas pasan el control de patrones y tipo, incluidos identificadores numéricos con puntuación interna; nombres y direcciones con comas no se presentan como listas.
Relaciones aritméticas entre columnas
- Reconoce una regularidad mediante un único soporte declarado (
umbral_aritmetica = 0.9) dentro de la tolerancia y, una vez reconocida, informa todas sus discrepancias sin aplicar un segundo filtro por su cantidad absoluta:max_violaciones_aritmeticase elimina. El soporte, el universo mínimo y la tolerancia quedan en la evidencia y el alcance. -
perfilar()descubre identidades aditivas y proporcionalidades estables entre columnas numéricas y las presenta como evidencia observada, no como reglas del dominio. Cada hallazgo declara proporción de cumplimiento, universo de filas finitas, tolerancia numérica, constante proporcional y filas discrepantes. -
umbral_aritmetica,min_filas_aritmetica,tolerancia_aritmeticaymax_columnas_aritmeticahacen visibles los supuestos y el costo del diagnóstico. Si el límite de columnas recorta combinaciones,cobertura_diagnosticoslo declara explícitamente.
Capa de marcos
-
regla_evaluacion()aceptaproporcion_minimapara declarar un veredicto sobre la proporción de medidas que cumple la condición. El objeto conserva el umbral; la evaluación muestra proporción, veredicto, componentes y universo, sin ponderar medidas ni crear un puntaje global. Las reglas por medida conservan su contrato y su estructura de salida. -
metrica()acepta las etiquetas relacionales degranularidades()—por ejemplo,"columna"— y guarda siempre su equivalente canónico de la ontología ("atributo"). Un valor inválido muestra ambos vocabularios. - El error de una regla que no engancha ninguna medida enumera lo solicitado y las métricas instanciadas disponibles, incluidos sus nombres calificados.
Perfilado de geometrías
- Las columnas
sfcinforman CRS, tipo de geometría, geometrías vacías e inválidas, coordenadas fuera del dominio declarado y caja envolvente. Una geometría sin CRS deja el conteo de dominio enNA: no se supone EPSG:4326. Las geometrías vacías se cuentan aparte y no integran el universo del chequeo de dominio. - Los tipos mixtos se comparan por familia: las variantes simples y
MULTIcompatibles conviven sin hallazgo, mientras que familias distintas yGEOMETRYCOLLECTIONse señalan. La validez declaravalidez_criterio = "planar"; sobre CRS geográficos un fallo planar es sospechoso y no afirma invalidez esférica. -
n_dominio_evaluadosyn_bbox_evaluadoshacen públicos los universos no vacíos de sus métricas;bbox_alcancedeclara que la caja usa las coordenadas crudas, incluidas las que estén fuera de dominio.n_validez_evaluadospublica por separado el universo de GEOS, incluidas las geometrías vacías.dimension_geometriadeclaraXY,XYZ,XYMoXYZM; Z y M quedan enumeradas endimensiones_no_evaluadasy generan una fila de cobertura. ParaXYMyXYZM, la validez topológica se calcula en XY después dest_zm()yvalidez_preprocesamientodeclara ese paso. - El control de dominio compara también las coordenadas transformadas con la
BBOXdel área de uso del WKT. Detecta, entre otros casos, grados donde el CRS espera metros; no detecta una zona UTM equivocada cuando las coordenadas interpretadas caen dentro del área de esa zona. Una caja mundial es un no-op evaluado y un WKT sinBBOXproduce una fila de cobertura, sin asumir alcance global. - Los nuevos hallazgos distinguen CRS ausente, geometrías inválidas o vacías, coordenadas imposibles y tipos geométricos mixtos. Si falta el paquete opcional
sf, el perfil no inventa ceros ni hallazgos: registra una fila condependencia = "sf"encobertura_diagnosticos.
Fechas con meses escritos
-
detectar_formatos_fecha()reconoce fechas con meses escritos en español (incluyesetiembreyset) y en inglés, además de los formatos numéricos existentes. La tabla de nombres es propia y no depende deLC_TIME, y sólo acepta la estructura completa de una fecha o de un mes con año: encontrarmarzodentro de una oración no convierte el texto en fecha. Los meses escritos desambiguan el día y el mes; los años de dos dígitos siguen siendo candidatos y no se les asigna un siglo en silencio. - Los períodos expresados sólo como mes y año declaran
granularidad = "mes"y no inventan el día 1 para calcular mínimos, medias o conversiones; esos resúmenes quedan enNAcon estadogranularidad_incompleta. Los años escritos en meses también se limitan al rango 1800–2100, como las fechas compactas. - La detección de meses sólo ejecuta sus expresiones regulares sobre los valores candidatos y reutiliza ese resultado al calcular el resumen de la columna. Así el texto libre que menciona meses no paga el costo completo ni se vuelve a analizar.
- Ese resultado intermedio se mantiene sólo durante el perfilado y no queda adjunto al objeto público
formatos_fecha. En columnas mixtas, los resúmenes de fecha se calculan sobre las fechas completas y declaran cuántas fechas de mes-año quedaron fuera; una columna compuesta sólo por períodos conserva el estadogranularidad_incompleta. -
inferir_tipo()tampoco conserva el caché interno de detección de meses. El diagnóstico de variantes del vocabulario sigue siendo una señal heurística: Jaro–Winkler puede acercar nombres de calles o códigos con prefijos compartidos y sus grupos deben revisarse como sospechosos, no como identidades. - El hallazgo de variantes del vocabulario sólo atribuye el límite de proporción cuando existe un grupo compatible que retener; si todas las cercanías fueron descartadas por secuencias numéricas incompatibles, lo informa con ese motivo.
Variantes del vocabulario
-
perfilar()agrega el hallazgocasi_duplicados_vocabulario: agrupa, por columna, variantes que la normalización funde o que quedan bajo el umbral de Jaro–Winkler y conserva la frecuencia de cada forma. La unidad es el valor distinto, no la fila; no se elige una forma canónica ni se modifica el dato. Las aristas de distancia forman estrellas alrededor de un valor de frecuencia estrictamente mayor y único; los empates no se fuerzan y no se cierra transitivamente una cadena de vecinos. Cada grupo declara su distancia mínima y máxima, ymax_proporcion_grupo_vocabulariopermite declarar que el diagnóstico no aplica cuando un componente abarca demasiado vocabulario; el filtro se activa desde 20 valores distintos o cuando el grupo mayor tiene al menos 10 variantes, y sólo suprime si la proporción también supera el umbral. Así no oculta grupos pequeños, pero tampoco entrega una columna entera como una sola familia. Cuando hay pares cercanos pero no una frecuencia central única, el alcance declara la falta de asimetría y apunta adetectar_duplicados_aproximados(). Las aristas de distancia con secuencias numéricas distintas se descartan; los ceros de relleno y separadores de miles se consideran equivalentes, pero una errata dentro de un número puede quedar sin agrupar deliberadamente. El alcance informa los pares descartados por números y separa el tamaño potencial del componente del tamaño que queda compatible con esa regla.casi_duplicados_vocabulario = FALSElo desactiva. El alcance declara los valores y pares comparados, los recortes y la ausencia destringdist; las fusiones exactas se siguen informando sin ese paquete. Los resultados del perfil pueden cambiar porque ahora se señalan estas variantes como evidencia para una revisión de vocabulario.
Referenciales
- Las métricas de referenciales heredan el perfil de
normalizardeclarado enreferencial()(o aceptan uno explícito), por lo que variantes de caja, acentos y espacios pueden pasar a reconocerse como presentes. Esto cambia los resultados de correctitud y cobertura de forma deliberada; las claves siguen evaluándose por identidad exacta. -
CorrectitudSemFuerteyCorrectitudSemDebilpueden agregar, sin cambiar el veredicto, el candidato más cercano y su distancia como evidencia. La proximidad usa Jaro–Winkler por omisión (p = 0.1, umbral0.10), sólo se calcula para fallos y declara sus límites o la ausencia destringdist. Se calcula sobre los valores fallidos distintos y se reparte a las filas repetidas; el alcance distingue filas fallidas, valores distintos y valores comparados.
Perfil de normalización para comparar
-
normalizardeja de ser sólo un interruptor lógico:TRUEconserva el caso común con minúsculas, espacios, acentos protegidos y comillas;FALSEdesactiva esos pasos configurables;"amplio", [normalizacion()] y una lista nombrada permiten elegirlos por columna. La representación normalizada sólo decide qué valores se comparan: nunca modifica los datos guardados. -
perfilar()conserva el perfil resuelto y los análisis de duplicados y claves lo heredan cuando recibennormalizar = NULL. Los resultados pueden cambiar porque el umbral se aplica sobre la cadena normalizada; el perfil informa, por vocabulario, cuántos valores fundió cada paso. - La comparación aplica siempre descomposición y orden canónicos en el subconjunto latino cubierto; no reordena palabras ni aplica abreviaturas de vías. Las claves siguen descubriéndose por identidad exacta y agregan la unicidad normalizada como métrica informativa.
- El informe de fusiones compara el perfil completo con una versión que apaga cada paso por separado: sus cifras no son aditivas y el total normalizado se informa aparte. Ahora usa el vocabulario completo (las fusiones son una propiedad de pares que una muestra de valores puede ocultar) y la normalización se aplica de forma vectorizada;
n_usadosy el estadoexactodejan explícito el alcance real. - El informe de fusiones no se calcula cuando
normalizar = FALSE, porque no hay pasos configurables que evaluar. Cuandoperfilar()ya lo calculó,detectar_duplicados_aproximados(perfil = ...)lo reutiliza en lugar de recorrer de nuevo el vocabulario. -
protegeracepta grafemas compuestos y el valor predeterminado conservag̃además deñyü, para no borrar letras guaraníes al comparar.
Diagnósticos de texto invisible
- Amplía la detección a los espacios Unicode, marcas direccionales, BOM y otros invisibles de transporte. Los espacios Unicode se pueden colapsar a espacio ASCII sólo mediante una acción explícita y destructiva; ZWJ/ZWNJ se informan pero se conservan. La comparación normalizada usa estas mismas clases sin borrar caracteres semánticamente significativos.
- El hallazgo de separadores en campo, su acción y su conteo usan nombres específicos para cubrir tabulaciones, saltos, avances de página y tabulaciones verticales.
-
perfilar()identifica controles C0/C1 e invisibles Unicode, entidades HTML reconocibles y separadores dentro de campos. La evidencia escapa esos caracteres (<U+200B>,\\t,\\n,\\r,\\f,\\v) y conserva los conteos por fila. - Los controles invisibles que no son separadores se pueden eliminar y se recomiendan por defecto; decodificar entidades HTML y reemplazar separadores de línea quedan como acciones explícitas porque pueden cambiar contenido legítimo. Las tres dejan el número de valores cambiados en el registro.
Reparación de texto y licencia
La medida predeterminada de duplicados ahora aplica Jaro–Winkler con
p = 0.1(el valor anterior era Jaro puro porp = 0) y el umbral pasa de0.12a0.10. Las dos decisiones pueden cambiar los pares informados al actualizar; el cambio es deliberado y queda declarado en la ayuda.Declara
cli (>= 3.0.0). El motor usa la interfaz de barras de progreso (cli_progress_bar()y sus compañeras), que existe recién desde esa versión; antes el requisito estaba supuesto y no escrito.Clasifica los duplicados exactos comparando los textos que realmente entran a la medida, después de normalizarlos, y no mediante igualdad exacta de un flotante. Esto hace el resultado independiente de la arquitectura y mantiene como
aproximadoun par de textos distintos aunquesoundexdevuelva distancia cero.Cierra el motor de reparación de texto:
decode_inconsistent_utf8trabaja por subcadenas con el detector de ftfy 6.3.1, conserva los estados parciales con U+FFFD y agrega tres extensiones deliberadas de badness sobre ftfy 6.3.1: la regla de inicio del issue #222, también discutida en el PR #232; la regla de caja que detecta mojibake de KOI8-R del issue #231; y la regla específica paraâdel issue #233. La tabla de bytes KOI8-R es la cuarta extensión y la puerta literalÃpara formas portuguesas y francesas es la quinta.Incorpora un motor R puro para detectar y reparar mojibake en varias codificaciones, inspirado en el diseño y las tablas de ftfy 6.3.1 de Robyn Speer. Los resultados distinguen reparaciones completas, parciales y casos irrecuperables; los estados llegan al hallazgo, al plan y al registro.
Completa el port de las reglas de detección y de los transcodificadores de ftfy: las transformaciones de bytes se encadenan antes de decodificar, las pérdidas quedan como U+FFFD y estado
reparado_parcialmente, y nunca se introduce un control invisible nuevo.Completa
restore_byte_a0de ftfy 6.3.1: conserva la frontera de la palabraà, respeta las excepciones portuguesas y cubre las seis formas de bytes alterados, sin partir ni pegar palabras.Conserva los espacios no separables y agrega el decodificador R puro de variantes UTF-8 de ftfy: combina pares CESU-8 y reconoce
C0 80, e incorpora la tabla de bytes KOI8-R adicional, con los estados y pérdidas ya declarados.Declara como quinta extensión deliberada la puerta adicional para la secuencia literal
Ã, que conserva las formas portuguesas y francesas observadas en padrones; el decodificador de variantes rechaza secuencias que producirían un NUL, en vez de omitir un carácter al materializar el texto.La licencia del paquete pasa de
GPL-2 | GPL-3aGPL-3; las partes derivadas del diseño de ftfy se atribuyen enLICENSE.notebajo Apache-2.0.La estrategia de reparación de texto se registra como
reparar_codificacion.
Recursos de comparación
- Fija por omisión en dos los hilos que
stringdistpuede usar en las comparaciones aproximadas y declara el valor efectivo enalcance. - El aviso interactivo del camino LSH identifica
nucleoscomo la perilla que puede acortar la etapa de comparación, sin prometer una ganancia fija. - La viñeta de escala documenta el rendimiento observado entre dos y treinta y un hilos y deja explícito que después de dieciséis no hubo una mejora medida.
- Documenta que el piso de tiempo de LSH cubre sólo la comparación de cadenas, no la firma, las cubetas ni el troceo; los resultados no dependen de la cantidad de hilos.
- Actualiza las mediciones de escala para anotar la configuración de hilos y evita presentar tiempos dependientes de la máquina como cifras exactas.
Marcos declarables y alcance internacional
- Incorpora validadores vectorizados de ISO 3166, ISO 4217, correo, Luhn y módulo 97, junto con un pack uruguayo de cédula y RUT. Los packs territoriales se pueden extender sin registrar estado global ni modificar el núcleo.
- Separa clasificar de proteger datos personales: las formas numéricas poco discriminantes se informan sin suprimir estadísticos, mientras nombres semánticos, correos y documentos verificados conservan la protección.
- Documenta los contratos de todos los puntos de extensión y añade
propiedades_metrica()para consultar la configuración admitida sin inspeccionar closures. - Incorpora
marco_iso25012()como adaptación opcional y explícita de las quince características de ISO/IEC 25012:2008. - Identifica el marco activo en cada fila de
cobertura_analisis(). - Permite declarar taxonomías dimensión-factor con
marco_calidad(), validar modelos contra ellas y calcular cobertura con AGESIC sólo como valor de fábrica mediantemarco_agesic(). - Permite construir familias de madurez con nombres y umbrales propios sin cambiar los tres perfiles incluidos.
- Hace que el vector de sentinelas numéricos sea una política completa:
numeric()los desactiva explícitamente. - Reconoce coma y punto decimal, separadores de miles simétricos, símbolos monetarios y prefijos con forma de código ISO 4217.
- Clasifica RUT, DNI y otros documentos con la etiqueta neutral
documento_identidad. - Permite conectar packs personales territoriales al mismo clasificador de
perfilar(), con tolerancia explícita de errores de digitación; los nombres semánticos (telefono,fecha_nacimiento, entre otros) conservan prioridad sobre formas numéricas genéricas.
Examinar datos
Detecta relaciones de orden sospechosas entre columnas numéricas o temporales comparables (por ejemplo,
inicio <= finymonto_bruto <= monto_neto). El hallazgo conserva los conteos y las filas fuera de orden, sugiere formalizar la regla conReglaIntegridadIntraEntidady declara enmeta$orden_columnaslas columnas y pares efectivamente comparados. Expone un filtro opcional de solapamiento intercuartil para tablas anchas; está apagado por omisión (umbral0) porque activarlo puede ocultar relaciones reales entre magnitudes de rangos distintos. Los pares descartados quedan contados en el alcance.Protege los estadísticos de orden y cuantiles de columnas personales, marca cada supresión en el objeto y conserva alertas de plausibilidad para fechas de nacimiento sin publicar sus extremos.
Añade
datos_operativos, un segundo conjunto sintético y neutral, reproducible desdedata-raw/, con problemas de calidad sembrados.Añade
analizar()como puerta de entrada al recorrido descriptivo, con cobertura conceptual y advertencias de alcance en el propio objeto.Incorpora distribuciones de valores acotadas, cuantiles, asociaciones de Pearson, V de Cramér y eta cuadrado, además de regularidad, duplicación, monotonicidad, cobertura, días de semana y huecos temporales.
Propone escalas de medición y roles sin confirmar lo que sólo se infiere de los valores; conserva niveles declarados, observados y ausentes.
Perfila tablas administrativas con métricas generales y por columna, proporciones en
[0, 1]y hallazgos filtrables.Descubre patrones de formato, tipos implícitos, formatos de fecha mixtos y ambiguos, años de dos dígitos, números regionales y problemas de codificación.
Detecta claves candidatas, relaciones, cobertura referencial, columnas y filas duplicadas, y dependencias funcionales exactas o aproximadas.
Conserva la ambigüedad día/mes con barra, guion y punto; reconoce fracciones de segundo y offsets ISO 8601.
Distingue NaN e infinitos, evita aproximar
integer64fuera del rango exacto dedoubley cuenta valores distintos en columnas de listas y geometrías.Clasifica posibles datos personales sin juzgar su presencia y protege por defecto los valores concretos cuando la evidencia es discriminante.
Normaliza factores a texto sólo en la operación, conserva
factoren el perfil y devuelve texto al transformar columnas factor conaplicar().Mantiene claves históricas estables en R 3.6 y fija explícitamente en UTC las fechas convertidas desde
Date.Las claves históricas tratan el texto ilegible (UTF-8 inválido) como ausente: comparte con
NAla marca~, en vez de intentar codificarlo como texto literal.Añade conteos explícitos de evaluados y afectados, con la unidad de conteo, a cada hallazgo; conserva NA cuando el alcance no permite conocerlos.
Añade trazabilidad acotada por hallazgo mediante índices de fila, con estados explícitos para lo disponible, truncado, no aplicable y no disponible; el reporte resume el estado sin imprimir los índices.
Medir y evaluar calidad
- Declara métricas genéricas, específicas e instanciadas con tipo de resultado y granularidad explícitos.
- Incluye veintiuna métricas automatizables, tres métricas tabulares basadas en referenciales y una correspondencia verificable con las 49 entradas del catálogo de AGESIC.
- Separa en el catálogo la disponibilidad de cada métrica de la causa o el matiz de esa disponibilidad, y documenta las 49 correspondencias sin vacíos.
- Ajusta las métricas oficiales de oportunidad al resultado booleano del marco y conserva la fórmula continua del curso CPAP bajo nombres
GradoOportunidad*. - Incorpora contratos explícitos
vigencia()yescala(), y una tabla de cobertura que distingue lo medido, no declarado, no aplicable y fuera de alcance. - Implementa las cuatro agregaciones del marco y la cadena de evaluación de medidas, reglas y perfiles de madurez. No calcula un índice global.
- Propone modelos editables a partir del perfil sin convertir observaciones de una sola entrega en requisitos silenciosos.
- Estima el costo antes de comparar, aplica un presupuesto de pares en los caminos exhaustivo y LSH, y publica el alcance de la estimación.
- Incorpora MinHash y LSH deterministas para generar candidatos a escala, con deduplicación por banda, garantía declarada y degradación explícita.
- Permite bloquear por una columna elegida por el usuario y estima los pares que el bloqueo puede dejar fuera, incluidos los ausentes como bloque propio.
Mejorar y monitorear
- Construye planes de limpieza editables con alternativas mutuamente excluyentes, justificación, modo guiado opcional y consentimiento adicional para eliminaciones.
- Aplica sólo acciones activas sobre una copia, conserva un registro y permite imputaciones confirmadas mediante dependencias funcionales exactas.
- Acumula evaluaciones en un histórico plano y versionado; detecta deriva del modelo y cambios estructurales entre perfiles.
- Procesa comparaciones exhaustivas por lotes con parciales en un directorio declarado, cruza los lotes sin pérdida de pares y deja constancia de que no son reanudables.
Informar
- Guarda y recupera análisis versionados sin datos de entrada por omisión y sin serializar entornos completos de reglas funcionales.
- Genera un único HTML autocontenido, en español, sin navegador, LaTeX ni recursos externos; los valores se escapan y la evidencia personal se enmascara por defecto.
