Changes in version 0.1.0 Ronda 36: la via DBI, las mediciones y la proteccion, refutadas sobre la ronda 35 La segunda ronda con el porton acotado al camino principal (perfilar() en memoria; perfilar_dbi() y las colecciones sobre PostgreSQL y SQL Server; sesiones UTF-8 y la C de un cron). Tres refutaciones sobre la ronda 35. La de la via DBI encontro dos altos -en SQL Server, una vista que fallaba dentro de una transaccion del usuario la deshacia sin aviso y la lectura seguia en autocommit; y, regresion de la ronda 35, las columnas con punto en el nombre se declaraban sin permiso- y cuatro medios; la de las mediciones, seis medios; la de la proteccion, cuatro altos -dos fugas de la coleccion y dos familias de formas comunes sin reconocer- y un medio por tapar de mas. Al unir los tres arreglos aparecio un defecto que ninguno tenia solo: la lectura nueva de los float8 y la proteccion nueva de la coleccion armaban juntas una consulta invalida; tambien se arreglo. Desde esta ronda, la severidad de la proteccion sigue una regla escrita: es alto solo publicar lo que el paquete ya clasifico como personal o no reconocer una forma comun de los datos reales; las formas raras son bajas y quedan documentadas, y tapar de mas una categoria comun es medio. La via DBI: - SQL Server: la via DBI mide @@TRANCOUNT y XACT_STATE() alrededor de cada lectura; si un error destruye la transaccion del usuario, levanta una condicion propia y no continua en autocommit. - SQL Server: los nombres de columnas usados por HAS_PERMS_BY_NAME() y UNMASK se citan con QUOTENAME(). - SQL Server: las columnas publicadas para una tabla homonima salen del mismo catalogo que gobierna el objeto leido. - Vistas: muestra_motor, el spool y bloque_filas aislan la columna culpable de una fila invalida; las columnas sanas conservan su diagnostico y el alcance omitido queda en cobertura. - Muestras DBI: el reintento del spool recorta la fuente muestreada desde una tabla derivada, sin duplicar LIMIT ni TOP y conservando el alcance pedido para el perfil local. - PostgreSQL: los tipos compuestos con todos sus campos nulos ya no se cuentan como faltantes. - Muestras: si orden_muestra queda fuera al aislar una columna, la metadata declara que no se aplico y que la muestra no es estable. - Colecciones: candidatas completamente enmascaradas quedan registradas en cobertura_podas. - ODBC: bajo una sesion no UTF-8, la conversion de texto no Unicode se decide por columna y por el tipo entregado por el controlador, no por la validez accidental de cada valor. - Colecciones: una clave de orden que no nombra una tabla declarada ahora es un error explicito. Las mediciones: - perfilar_dbi() conserva los dobles float8 de PostgreSQL cuando la sesion escribe pocas cifras y centra el desvio con la misma aritmetica que recibe R. - SQL Server calcula el desvio decimal en FLOAT y no depende de haber pedido basicos; la via por columna obtiene el centro cuando hace falta. - Los centinelas fraccionarios declarados sobre real se reconocen aunque el controlador entregue el valor float32; la corroboracion considera equivalente esa representacion. - comparar_perfiles() declara como no_evaluado un diagnostico recortado por un tope y compara pares de columnas por su contenido, no por su posicion. - Las muestras truncadas declinan los hallazgos que no pudieron evaluarse, en vez de publicarlos como resueltos. - Limitacion conocida: la tolerancia absoluta de la corroboracion tambien se suma al desvio, por lo que puede ocultar diferencias pequenas frente a un desvio menor que 1e-4. La proteccion: - En una coleccion, la copia sin verificador escrita como texto de cifras en OTRA tabla se protege. Con conservar_perfiles = TRUE, una columna de tramites con la cedula de personas escrita "4.685.467,00", "4 685 467.0", "4.685467e6" o con "S/D" entre sus valores no era candidata a copia: la coleccion solo probaba las columnas que publicaban un extremo numerico de siete cifras o las que la muestra clasificaba como documento, y esas se infieren texto o identificador. El perfil conservado publicaba su moda, sus ejemplos y la evidencia de sus hallazgos (22 apariciones de 10 cedulas en PostgreSQL 16 y SQL Server 2022), mientras perfilar() sobre las dos tablas juntas la protegia. Ahora tambien es candidata toda columna que no es de numeros ni de fechas cuyo patron en la muestra son cifras con separadores o con exponente; se prueba con la misma regla, asi que una de mas solo cuesta su consulta. Medido: 0 apariciones en los dos motores. - La coleccion lee las candidatas a copia convertidas, como perfilar_dbi(). La copia en una columna money de PostgreSQL -o real con extra_float_digits = 0- se leia cruda: en ceros, o redondeada a seis cifras, y la prueba no la reconocia, mientras el perfil, que la leyo convertida, publicaba su minimo, su maximo, su mediana y su media. Las dos lecturas de valores de la coleccion -las columnas protegidas y las candidatas- usan ahora una sola expresion (.expresion_lectura_coleccion()). Medido en PostgreSQL 16: money y real protegidas, extremos NA. - Las formas comunes de los datos de la region se reconocen en una columna calificada. Telefonos con puntos de a dos cifras ("099.12.34.56"), con el .0 de una planilla ("59899123456.0"), con 00598 o 598 pegado, con el espacio duro o la raya de Unicode entre los grupos; domicilios con Br., Cno., Rbla., Pje., Gral., Dpto., Mza.; nombres con una inicial ("Juan A. Perez") o un O', con espacio duro, o con acentos en latin1 sin marca -un CSV de Windows leido bajo C-; y fechas de nacimiento o de defuncion en una columna calificada como categoria (nacimiento_estado). Todas se publicaban; ahora se protegen, por perfilar() y por medir(), tambien bajo C. - Lo que la ronda 35 tapaba de mas se publica. Importes con separadores de miles o redondos (importe_cliente con "1.250.000", importe_telefono con 1250000), codigos con un ano (cliente_id con "2024-00001"), medios (medio_pago_cliente: la palabra de persona ahora tambien la califica medio), lugares de nacimiento (pais_nacimiento, y el "Uruguay" que tapaba en pais_destino), estados de una baja (usuario_baja con "Sin baja", "Baja definitiva"), categorias con una palabra que tambien es un nombre de pila ("Primer Pilar", "Convenio Marco", "Turno Domingo", "Plan Max": pilar, marco, norma, franco, domingo, julio, blanca, celeste, rosa y max salieron de la lista) o una marca de calle ("Radio 5 km", "En ruta") y los metros de una calle (calle_metros: una columna de numeros es un domicilio solo con una palabra de clave, barrio_id). Medido celda por celda contra proteger_datos_personales = FALSE: las mismas modas y extremos. - Limitaciones conocidas (bajas), declaradas en la ayuda. Las escrituras raras de un numero -coma decimal con exponente, "4.685.467,00e0"- no se leen como copia; un nombre de pila que es tambien una palabra corriente (Max, Pilar) no cuenta como senal, y en una columna de tres personas basta una con ese nombre para que no llegue al 80 % ("Nunez Bruno", "Diaz Sol", "Acosta Max"); y cuando la coleccion vuelve a perfilar una tabla por una copia, resumen_coleccion, cobertura_coleccion, cobertura_metricas y meta$lecturas quedan con el primer perfil (sus estados dicen calculado donde el perfil dice que se ocultaron; no publican valores). Lo que aparecio al unir: - Una coleccion con una columna float8 de PostgreSQL verifica su proteccion. La lectura de los float8 se proyecta con un alias propio, y la proteccion de la coleccion usaba la lectura publicada en meta$lectura_convertida dentro de SELECT ... AS valor ... GROUP BY. Con el alias adentro la consulta no se podia preparar: con una cedula guardada como double precision, la coleccion declaraba la proteccion sin verificar y, del lado seguro, tapaba columnas que no son personales (el minimo y el maximo de un importe). Ahora la lectura publicada es la expresion sola, como la de money y real, y el alias va solo en la proyeccion. Medido en PostgreSQL 16: la copia sigue tapada y el importe publica. La ayuda: - ?perfilar_dbi declara en "Motores medidos" que motores, versiones y controladores son el camino que se mide en cada version, y que con otro motor la funcion corre por la via portable sin cifras comprobadas contra el. - La referencia del RFC 5322 en ?validadores_formato apunta a la direccion vigente del editor de RFC. Ronda 35: la via DBI otra vez, las mediciones, la proteccion y la comparacion de perfiles La primera ronda con el porton acotado al camino principal (perfilar() en memoria; perfilar_dbi() y las colecciones sobre PostgreSQL y SQL Server; sesiones UTF-8 y la C de un cron). Tres refutaciones sobre la ronda 34. La de la via DBI encontro cinco altos -una columna con DENY SELECT hacia caer a R en SQL Server; bajo C los varchar con tilde de SQL Server se leian como bytes invalidos y perdian sus relaciones; con enmascaramiento dinamico las columnas completas salian vacias; relaciones_coleccion() leia money de PostgreSQL como 0; dentro de una transaccion de PostgreSQL perfilar_dbi() abortaba y la dejaba inservible- y cinco medios; la de las mediciones, cuatro medios, dos de ellos regresiones de la ronda 34 -el real de PostgreSQL anterior a la 12 y el desvio centrado del real en SQL Server-; la de la proteccion, tres medios, uno de ellos regresion de la ronda 34 -los nombres de personas en columnas como usuario_alta o nombre_jefe_grupo-. Sobre los arreglos, antes de unirlos, una segunda refutacion de otras familias de modelos encontro que el arreglo de la proteccion tapaba de mas categorias de dos palabras, que comparar_perfiles() callaba los hallazgos de varias columnas y que con universo = "muestra_motor" una columna sin permiso desaparecia sin aviso; tambien se arreglaron. Los permisos por columna (alta en SQL Server, media en PostgreSQL): - Una columna con DENY SELECT ya no hace caer a R. En SQL Server por odbc + FreeTDS, perfilar_dbi(), plan_perfilado_dbi(), perfilar_coleccion() y relaciones_coleccion() sobre una tabla con una columna negada morian con una violacion de segmento, en UTF-8 y bajo C: la lectura conjunta del esquema nombraba la columna, el motor la rechazaba, y una consulta PREPARADA rechazada por un permiso de columna deja la conexion de odbc rota; la siguiente caia. Medido: la misma consulta sin preparar falla con su error y la conexion sigue. Ahora las columnas de la tabla y lo que la credencial puede leer de cada una salen del catalogo del motor antes de leer -sys.columns con HAS_PERMS_BY_NAME() en SQL Server, pg_attribute con has_column_privilege() en PostgreSQL, legibles por cualquier rol-, de una sola funcion que usan las cuatro puertas. La columna negada no se nombra en ninguna consulta, aparece en resumen_tabla$columnas con sus metricas no_disponible y una fila de cobertura con el motivo y el remedio, y las demas se perfilan enteras. SQL Server rechaza tambien COUNT(*) y SELECT 1 ... WHERE 1 = 0 sobre una tabla con una columna sin permiso (medido): la lectura va sobre una tabla derivada con las columnas legibles. Y con odbc toda consulta se ejecuta sin preparar (immediate = TRUE, como el dbGetQuery() de odbc), asi que lo que el catalogo no ve falla con su error y no rompe la conexion (medido anulando la consulta al catalogo: perfilar_dbi() declara la columna que el motor rechazo y relaciones_coleccion() el par, sin caida; los datos y dbColumnInfo() son los mismos con las dos formas sobre 22 tipos de SQL Server). - Con GRANT SELECT por columnas en PostgreSQL la tabla tiene sus cuatro columnas. dbListFields() de RPostgres lee information_schema.columns, que filtra por privilegio: perfilar_dbi() perfilaba 3 de 4 columnas y perfilar_coleccion() publicaba n_columnas = 3 sin decirlo. Ahora la cuarta se declara como en SQL Server. - relaciones_coleccion() compara el par cuando una columna no se puede leer. Sin columnas_candidatas la lectura era SELECT *, que con un permiso por columna se rechaza entero: en PostgreSQL el par se perdia con el nombre de la tabla tapado y un remedio de permisos de tabla, y en SQL Server caia R. Ahora la lectura nombra las columnas legibles y las combinaciones de la que no se leyo quedan en cobertura_podas con motivo columna_sin_permiso. Una candidata que existe y no se puede leer ya no se rechaza como inexistente. - Con universo = "muestra_motor" la columna sin permiso tampoco desaparece. La via del spool -la de muestra_motor con muestra, tambien desde perfilar_coleccion()- armaba resumen_tabla$columnas solo con las columnas legibles y no declaraba las demas: una tabla de seis columnas con cuatro negadas salia con dos y 0 filas de cobertura, y la coleccion con n_columnas = 2 (medido en SQL Server; lo encontro una refutacion externa). Ahora la columna esta en resumen_tabla$columnas con sus metricas no_disponible y una fila de cobertura con el motivo, como sobre la tabla completa y por bloques. La misma omision valia antes para cualquier columna que el esquema no pudiera leer (un nombre que la sesion no puede escribir, una que el motor rechaza). El enmascaramiento dinamico de SQL Server (alta): - Una columna enmascarada publica su completitud real y nada de lo que describe la mascara. Con MASKED WITH y una credencial sin UNMASK, COUNT(col) sale enmascarado -0- y perfilar_dbi() y perfilar_coleccion() publicaban 100 % de faltantes en columnas completas (medido: 400 de 400 donde habia 0, 0, 0 y 40), sin aviso con bloque_muestra = "solo_agregados"; con la muestra, divergencias y "un unico valor no ausente" sobre xxxx; y relaciones_coleccion() publicaba cedula -> cedula con cobertura 1 entre dos columnas enmascaradas sin un valor comun. Ahora el paquete lee sys.masked_columns y el permiso UNMASK (de la columna, 2022, o de la base): la completitud se mide, y distintos, moda, extremos, media, mediana y desvio quedan no_disponible con el motivo; la columna no entra en perfil_muestra (fila alcance_distinto) ni en las relaciones (columna_enmascarada en cobertura_podas). Con UNMASK la columna se perfila entera (medido con GRANT UNMASK ON t (cedula)). El conteo de faltantes (media): - Los no nulos se cuentan con una sola expresion en todos los motores. COUNT(CASE WHEN col IS NULL THEN NULL ELSE 1 END) reemplaza a COUNT(col) en la consulta compartida, la por columna, la de lotes de distintos, el desvio y la mediana escalar. SQL Server rechaza COUNT() sobre text, ntext e image, y la completitud de esas columnas quedaba no_disponible (medido: 10, 10 y 0 nulos de 100 que el motor sabia contar); ahora se mide. Medido en siete motores (SQLite, duckdb, PostgreSQL 16 y 9.3, SQL Server, MySQL, MariaDB): las mismas cifras, con muestra, sin ella y por bloques; en una tabla de 3.000.000 de filas la consulta tarda 0,102 s contra 0,094 s en PostgreSQL y 0,155 contra 0,140 en SQL Server, y perfilar_dbi() entero lo mismo dentro del ruido. La muestra de una vista que falla en una fila (media): - La columna que hace fallar la muestra se aisla sobre las filas de la muestra. Una vista que convierte un texto sucio en la fila 150 dejaba sin perfil_muestra a TODAS las columnas con un motivo falso ("ninguna fallo por si sola"): la sonda que busca la columna culpable leia una fila, y la fila 1 era sana. Ahora la sonda es la misma lectura de la muestra con menos columnas: la culpable se declara (alcance_distinto) y las demas se perfilan (medido en PostgreSQL y SQL Server). Limitaciones conocidas (bajas): - En SQL Server, una columna sin permiso detras de un sinonimo, de una tabla de otra base o de una tabla temporal no se ve en el catalogo: la tabla da el error de la via (lupa_error_tabla_dbi) en vez de perfilar las demas columnas. La conexion sigue. - Con universo = "muestra_motor" y una columna sin permiso en SQL Server, TABLESAMPLE no se puede escribir sobre la tabla derivada: la muestra del motor usa otra forma que el motor acepte (medido: NEWID() con limite) o se degrada con su motivo. La via DBI y las colecciones: - Dentro de una transaccion del usuario la via ya no la aborta. En PostgreSQL un error cualquiera aborta la transaccion abierta, y la via manda consultas que pueden fallar a proposito -la sonda de dialecto SELECT STDEV(1.0), hecha para que el motor la rechace- o por el dato -COUNT(DISTINCT) de un json, la vista rota-: dentro de una transaccion REPEATABLE READ -lo que recomienda el propio motivo de consistencia- perfilar_dbi() abortaba sobre una tabla sana con lupa_error_conteo_dbi ("current transaction is aborted"), perfilar_coleccion() no perfilaba ninguna tabla, y la conexion del usuario quedaba inservible hasta su ROLLBACK. Ahora, con una transaccion abierta en PostgreSQL -la de dbBegin(), o un BEGIN mandado a mano, que se reconoce por el motor-, cada sentencia de la via va entre un SAVEPOINT propio y su RELEASE, y si falla se vuelve con ROLLBACK TO SAVEPOINT: por construccion, en los dos envios por los que sale toda consulta y en los dos metadatos. El paquete nunca manda BEGIN, COMMIT ni ROLLBACK, y fuera de una transaccion no agrega nada. Medido en PostgreSQL 16 y 9.3, dentro de dbBegin() y de un BEGIN crudo: perfilar_dbi() sobre una tabla sana, una con json, una vista rota y una con money, plan_perfilado_dbi(), perfilar_coleccion() de seis tablas y relaciones_coleccion() dan lo mismo que fuera, y el INSERT que el usuario hizo antes en su transaccion sigue ahi -y sin confirmar-. duckdb tambien aborta la transaccion por un error al ejecutar y no tiene SAVEPOINT: dentro de una transaccion de duckdb un objeto que falla al leerse la deja abortada (documentado en ?perfilar_dbi, seccion "Dentro de una transaccion"). - relaciones_coleccion(muestra = Inf) ya no manda LIMIT Inf. format(Inf) escribia una consulta que todo motor rechaza -PostgreSQL busca una columna inf-: fuera de una transaccion costaba una consulta rechazada por tabla, y dentro de una la abortaba (0 relaciones de 3). Sin tope se lee directo por la via sin LIMIT. - relaciones_coleccion() lee las columnas money de PostgreSQL convertidas. RPostgres entrega 0 en toda celda money, y MIN/MAX tambien: la coleccion inventaba fac.importe -> nota.importe m:m con cobertura 1 entre dos tablas sin un valor comun, y podaba con rangos_disjuntos fac.importe -> cobro.monto, que comparte 60 importes (a mano en el motor: 60 de 60). La ronda 34 lo habia cerrado en perfilar_dbi(); la lectura de cada tabla de la coleccion, la consulta de rangos de la poda y la lectura de valores protegidos de perfilar_coleccion() usan ahora la misma expresion, .lectura_convertida_dbi() (una consulta de cero filas por tabla, solo donde la lectura convierte algo). Una prueba recorre las cuatro puertas exportadas sobre una conexion que lee money como RPostgres y no encuentra ninguna lectura cruda. - El texto varchar que odbc entrega en latin1 se lee como latin1. Bajo el locale C -el de un Rscript de cron- FreeTDS entrega las columnas de texto no Unicode en latin1 y odbc las marca UTF-8 sin convertirlas: sobre una base SQL Server con cotejamiento latin1, perfilar_dbi() publicaba "Hay texto con secuencias de bytes que no forman UTF-8 valido" sobre una base que no tiene ninguno, y relaciones_coleccion() perdia nombre -> departamento (200 de 200 en el motor) entre un nvarchar y un varchar. Todo valor que llega del motor pasa por un mismo punto, que lee como latin1 el texto de odbc marcado UTF-8 que no es UTF-8 valido. Lo que no se puede recuperar -un caracter fuera de latin1 llega ?- se declara: cada columna de texto no Unicode leida bajo una sesion que no es UTF-8 queda degradado en resumen_tabla$cobertura con el remedio (un locale UTF-8), y la coleccion lo dice en meta$lecturas$motivo. Medido bajo C, C.UTF-8 y es_UY.UTF-8: la misma relacion y el mismo hallazgo en las tres. - Con la referencia truncada, la FK que no alcanza el umbral se declara aunque otra columna del par lo alcance. El "sin conclusion" de relaciones_coleccion() solo se publicaba si NINGUNA combinacion del par llegaba a umbral_cobertura: un indicador activo de dos valores, con cobertura 1 trivial, borraba de toda salida id -> cliente_id (0,4996 sobre las 10.000 filas leidas, 5.000 de 5.000 en el motor). Ahora el par queda en cobertura_pares con una fila: cuantas combinaciones no alcanzaron el umbral, las cinco de mayor cota, y cuantas lo alcanzaron. - Bajo C, orden_muestra con una columna que la sesion no puede escribir da el motivo de la codificacion. perfilar_dbi() con una columna con tilde en orden_muestra, por odbc, decia "El motor rechazo las columnas de orden_muestra", y el motor no habia recibido nada: ahora lupa_error_codificacion_dbi con el remedio del locale UTF-8. - Los motivos de relaciones_coleccion() nombran la tabla, el esquema y las columnas. Se tapaban con [literal omitido] -tambien la columna de orden que el usuario escribio y un mensaje propio del paquete-; desde la ronda 34 eran intocables en perfilar_dbi() y en perfilar_coleccion(), y ahora tambien aca. - columnas_candidatas sigue la regla de mayusculas de orden. Sobre SQL Server IMPORTE daba "nombra columnas inexistentes" mientras orden = "ID" se aceptaba; las dos resuelven como orden_muestra de perfilar_dbi() -la grafia exacta y, si no esta, sin distinguir mayusculas-, y un argumento mal formado de relaciones_coleccion() da lupa_error_argumento_dbi y no un simpleError. Las mediciones (medios): - En PostgreSQL, una columna real se lee por la forma decimal mas corta que vuelve al valor guardado, calculada en el motor -el valor se redondea a 6, 7, 8 y 9 cifras y se queda la primera que vuelve al mismo real-, y no por el texto con que la sesion lo escribe. Antes de PostgreSQL 12, o con extra_float_digits de 0 o menos en cualquier version, ese texto tiene seis cifras: en un 9.3, 200 enteros distintos de siete cifras salian 21 distintos, con un minimo 1234500 que no estaba en la columna y desvio 58,07 contra 57,88, en el resumen SQL y en la muestra a la vez y sin ninguna divergencia. La ronda 34 habia llevado esa lectura tambien al resumen SQL. Ahora la lectura da el valor guardado en cualquier version y con cualquier extra_float_digits desde -6, sin tocar el estado de la sesion; desde PostgreSQL 12 el texto de la sesion se prueba primero, asi que con el valor por omision la cifra es la de la ronda 34 (21.3). Medido en 9.3.25 y 16.15 con extra_float_digits -6, 0, 1 y 3 sobre 20.347 valores. Limitacion conocida, documentada: por debajo de -6 el servidor escribe todo doble con menos de nueve cifras (refutacion 35-D, M1). - El desvio centrado de perfilar_dbi() resta en doble sobre una columna real de SQL Server y una FLOAT de duckdb: en los dos real - decimal es real, y el desvio centrado de la ronda 34 se apartaba 1,4e-8 del de los valores guardados -peor que el de una pasada que vino a reemplazar-; la corroboracion lo publicaba como divergencia culpando al motor o al snapshot, y perfilar_coleccion() subia la tabla a alcance = "divergencias". Lo decide la funcion que arma la expresion, por el tipo declarado: una columna decimal o entera sigue restando en su tipo, que es exacto. Medido en los seis motores (refutacion 35-D, M2). - Una columna numeric o decimal con un valor de al menos 2^53 deja sus siete metricas de magnitud en no_disponible, como un entero de 64 bits, y con bloque_filas tambien n_distintos y la moda. La guarda miraba un texto numeric(20,0) que ni RPostgres ni odbc publican -dan numeric y el codigo ODBC 2-, asi que nunca se disparaba: con bloque_filas, 60 valores distintos salian 4, con una moda que no estaba en la columna y el desvio de los dobles, todo calculado y sin aviso. Ahora decide el valor: un doble desde 2^53 que viene de un decimal ya no es exacto (refutacion 35-D, M3). - comparar_perfiles() publica no_evaluado -y no resuelto, ok- el hallazgo de un diagnostico que el perfil nuevo apago o cuya declaracion retiro: casi_duplicados_vocabulario = FALSE -lo que ?perfilar recomienda para tablas grandes-, ausencia_estructural = FALSE, y una columna retirada de columnas_sin_ceros, columnas_no_negativas o columnas_personales. El dato estaba en meta y la comparacion solo lo leia para clave. Por construccion: cada argumento de perfilar() esta clasificado -habilita un diagnostico, es una vara, tiene su fila de configuracion, acota el trabajo o no cambia los hallazgos- y una prueba exige que la lista coincida con los argumentos, para que el proximo no quede afuera (refutacion 35-D, M4). La segunda vuelta encontro dos que la lista dejaba pasar: una clave declarada sobre OTRA columna -con id repetida y despues clave = "ci", clave_no_unica salia resuelto, ok- y duplicados_aproximados, que estaba clasificado como un argumento que no produce hallazgos: apagarlo, o correrlo sobre otras columnas, publicaba un par de columnas duplicado como resuelto o no publicaba nada. Ahora los dos salen no_evaluado, igual que una columna retirada de aplicabilidad (valor_fuera_de_aplicabilidad); un cambio de los parametros de duplicados_aproximados sobre las mismas columnas se declara en la fila configuracion_duplicados_aproximados, y un hallazgo que aparece porque el perfil nuevo encendio lo que lo habilita dice en su descripcion que el anterior no lo evaluaba. La guarda ya no mira solo que la lista tenga todos los argumentos: perfila un dato fijo con cada uno movido y exige que su clase explique lo que cambia (refutacion 35-D, segunda vuelta). - comparar_perfiles() compara los hallazgos de varias columnas -un par de columnas duplicado (nombre, ciudad), una relacion de orden o aritmetica (doble,monto)-. La guarda que no presenta como resuelto el hallazgo de una columna retirada preguntaba si su texto era una columna del perfil nuevo, y el de varias no lo es nunca: con el diagnostico encendido en las dos corridas y los datos cambiados, 100 pares duplicados y una relacion aritmetica desaparecian sin ninguna fila. Ahora solo se descarta si nombra una columna que desaparecio (refutacion 35-D, segunda vuelta). - La corroboracion de perfilar_dbi() no compara lo que un bloque no calculo: una metrica que el resumen SQL declara no_disponible -el motor rechazo la consulta sobre el tipo (json, xml, point y bytea de PostgreSQL; text, ntext, image, xml, geography y varbinary(max) de SQL Server), una guarda la retuvo-, la moda que la muestra no pudo contar -una columna de blobs- y las metricas de una columna compuesta. Se publicaban como divergencia (faltante_un_lado) y perfilar_coleccion() subia la tabla a alcance = "divergencias" pidiendo releer bajo una lectura consistente, que da lo mismo. Lo que no se compara queda en meta$corroboracion_bloques$no_comparadas con su causa. Un NaN o un infinito, y un valor guardado como texto en una columna numerica, se siguen comparando (refutacion 35-X, M1). Las mediciones (bajos): - comparar_perfiles() declara en la fila configuracion_umbrales las varas que cambiaban hallazgos y severidades sin decirlo: los umbrales de las relaciones de orden y aritmeticas entre columnas, los del vocabulario, datos_personales_permitidos, validadores_personales, umbral_documento_verificado y muestra_validadores. perfilar() registra en meta las siete que no guardaba. Una vara que un perfil de una version anterior no registra no se compara (refutacion 35-D, B2). - En SQL Server, el centro del desvio de una columna de coma flotante va como literal float: escrito como decimal, la media de una columna de magnitud 1e40 o 1e-25 tenia mas de 38 cifras, SQL Server rechazaba la consulta centrada y quedaba, sin decirlo, el desvio de una pasada (2,7e-3 relativo). Limitacion conocida, documentada en ?perfilar_dbi: en una columna cuyos valores son menores que 1e-10 en valor absoluto, la tolerancia absoluta de la corroboracion cubre cualquier diferencia (refutacion 35-D, B1). - Un desvio 0 sobre una columna con valores distintos se vuelve a pedir centrado aunque el minimo y el maximo como dobles coincidan -un numeric cuyos valores difieren por debajo de la resolucion del doble-: el motor cuenta los distintos, y centrado la resta se hace en numeric. El motivo ya no culpa solo a "una pasada" cuando la causa es la conversion a doble (refutacion 35-D, B3). La proteccion: - Una palabra de tipo personal calificada por el resto del nombre se decide con sus valores. La ronda 34 decidia solo por el nombre: usuario_alta, usuario_baja, funcionario_alta, funcionario_baja, responsable_grupo, responsable_canal o nombre_jefe_grupo con nombres completos, usuario_alta_login con logins, celular_estado, celular_alta, telefono_tipo o contacto_medio con celulares, y domicilio_alta, direccion_desde o direccion_tipo con domicilios dejaron de protegerse y publicaban su moda, sus ejemplos, la evidencia de sus hallazgos y, por bloques, cada valor distinto en meta$familias (117 a 120 de 120 en PostgreSQL y SQL Server). La ronda 33 los tapaba: era una regresion. El tipo de los valores solo servia para EXCLUIR -numeros, fechas, logicos-, nunca para ver si el calificador decia la verdad. Ahora la columna calificada es personal si sus valores lo muestran, con el tipo de los VALORES y no el de la palabra, en el 80 % de sus distintos: un correo (una @, tambien de una intranet), un telefono escrito como telefono (con +, cero inicial, separadores, interno o en una lista; sin centavos; con la palabra del telefono, tambien las cifras sueltas), un domicilio con marca de calle (Av., calle, Bvar., esq., apto... en el 30 %) o un nombre con un nombre de pila conocido, sin cifras, sin palabras de categoria (sin, en, por) y sin empezar como un lugar (San, Villa). Y la palabra de quien actua (usuario, funcionario, responsable, solicitante) con una de auditoria (alta, baja) cae con cualquier texto que no sea una fecha: un login "jperez" o un apellido solo dicen quien dio el alta. La palabra de nombre (nombre, apellido) vuelve a expresarlo en cualquier lugar del nombre, como dice la ayuda (nombre_jefe_grupo con apellidos solos o logins, nombre_grupo como nombre_departamento), y una columna de numeros no es un nombre (cantidad_nombres). Una primera version tomaba la forma sola como senal -dos palabras con mayuscula eran un nombre, una palabra con un numero un domicilio, y la palabra de fecha caia con cualquier texto- y tapaba tipo_cliente con "Gran Empresa", grupo_usuario con departamentos, estado_socio con "Pendiente de Pago", tipo_domicilio con "Oficina 101", cliente_desde con "Sin fecha", usuario_tipo con productos, importe_telefono con importes con centavos y codigos como "NORTE01"; la segunda refutacion lo midio, y ahora publican como en la ronda 34. Medido: las 24 columnas de la primera refutacion en memoria y por perfilar_dbi() en PostgreSQL 16 y SQL Server 2022, cada una como en la ronda 33 o como lo decidio la ronda 34; y las de la segunda -celulares en responsable_canal, nombres en contacto_alta, domicilios en cliente_tipo, correos en correo_alta, el mismo apellido en responsable_estado, dos personas, tres Maria- protegidas, con sus categorias publicadas; por las dos puertas (perfilar() y medir()). Una prueba recorre cada palabra calificadora de cada tipo con valores personales de los cuatro tipos y con los de su clase. Limitacion conocida (baja): la lista de nombres de pila no es completa; una localidad con nombre de persona ("Jose Ignacio") se lee como un nombre; y con una palabra de categoria o de clave, un login de solo letras o un apellido solo (responsable_grupo con "Rodriguez", usuario_id con "jperez_01") no se distinguen de una categoria o de un codigo y se publican, como un domicilio sin marca de calle: se declaran en columnas_personales. - La copia sin verificador escrita como numero con decimales en un texto se protege entera. Una columna de texto con la cedula de cada fila sin su verificador como la deja una planilla -"4685467.0", "4685467,00", "4.685.467,00", o mezclada con "4685467"- no se reconocia: la prueba de la copia leia sus digitos (46854670) mientras el resumen leia el numero 4685467 y lo publicaba como su minimo; publicaba tambien sus ejemplos y, por bloques, cada valor (136 a 155 apariciones en PostgreSQL y SQL Server). Ahora el texto que el paquete lee como numero da a la prueba las cifras de ese numero, con la misma lectura que el resumen (.leer_numeros_texto()); un texto con decimales que no son cero no es un documento. Tambien con signo o en notacion cientifica ("+4685467.0", "4685467e0"), que el paquete lee como numero, y con espacios de miles y ceros decimales ("4 685 467.0"), que no lee pero cuyo entero es el mismo; un texto con centavos que no son cero ("24 997.16") ya no se lee pegando los centavos, que inventaba una cifra que podia ser una cedula y tapaba el importe. Un importe con decimales en un texto pasa la misma prueba binomial que el mismo importe guardado como numero, y publica sus extremos (medido: 150 importes de siete cifras como "5460537.00" al lado de 150 cedulas). El tapado de mas de la regla con cedulas concentradas por tramos sigue sin medir: este cambio no toca la prueba, solo como llegan las cifras de un texto. Decision documentada: la regla compara contra los documentos protegidos, y sin la columna de la que copia -la tabla de pagos perfilada sin la de personas, fuera de una coleccion con perfiles conservados- una columna de siete cifras no se distingue de un importe y publica sus extremos. - En una coleccion, la copia sin verificador de los documentos de OTRA tabla se protege entera. Con conservar_perfiles = TRUE, una tabla de tramites que referencia a la persona por su cedula sin verificador quedaba documento_identidad debil y publicaba en resumen_tabla y en perfil_muestra su minimo, su maximo y su mediana, que son cedulas de la tabla de personas (6 apariciones en SQLite, PostgreSQL 16 y SQL Server 2022), aunque la ayuda promete proteger cada perfil con los valores de las demas tablas "con la regla de perfilar()". No era regresion: la regla de la copia corria con los documentos de la propia tabla. Ahora la coleccion prueba, con la misma regla (.copia_de_documentos()), cada columna no protegida que publica un numero de siete cifras o mas -o que la muestra clasifico como documento- contra los documentos de las demas tablas, sobre los valores distintos de la tabla entera leidos de la base (una consulta por columna candidata); la tabla con una copia se vuelve a perfilar con esa columna declarada personal ("documentos de la tabla personas sin su verificador"), y si la lectura o el nuevo perfil fallan, el perfil se cierra como el de una tabla ilegible. El orden de las tablas no cambia lo que se publica. Medido: 0 apariciones en los tres motores, con y sin bloques; el importe de siete cifras de la misma tabla publica sus extremos. - Dos limitaciones conocidas (bajas) de perfilar_dbi(), declaradas en la ayuda. La copia sin verificador se clasifica sobre la muestra: con una muestra que ve menos de tres de sus valores distintos -chica y ordenada sobre una tabla agrupada por persona- no se clasifica, y el resumen de la tabla entera publica sus extremos, que pueden ser documentos de personas que la muestra no vio; por bloques, cada valor en meta$familias. Y por la via por bloques, meta$familias guarda cada valor distinto de toda columna no protegida: una columna debil con las cedulas sin verificador de OTRAS personas -un garante- publica ahi cada una, donde perfilar() publica sus dos extremos. Verificar la copia sobre la tabla entera costaria una consulta por columna candidata en cada perfilar_dbi() con muestra parcial; una muestra mayor o aleatoria, o columnas_personales, lo evitan. Ronda 34: la proteccion, las mediciones y la via DBI Tres refutaciones sobre la ronda 33. La de las piezas compartidas encontro tres altos -con orden, relaciones_coleccion() perdia todas las relaciones sin aviso; un nombre de tabla latin1 dejaba rota la conexion odbc; coleccion() cargaba RMySQL sobre una conexion RMariaDB y la dejaba inservible- y uno medio; la de las mediciones, siete medios -la corroboracion de perfilar_dbi() acusaba como divergencia lo que una declaracion o la precision del motor hacen distinto, comparar_perfiles() no declaraba columnas_opcionales, money de PostgreSQL se leia como 0-; la de la proteccion, cuatro medios -una columna que copia las cedulas sin su verificador publicaba sus extremos, y dos reglas tapaban de mas-. Al arreglarlos aparecio uno mas, alto: el desvio de una pasada de SQL Server publicaba 0 sobre marcas de tiempo en milisegundos. La proteccion: - perfilar_dbi() publica los estadisticos de una columna que comparte el tipo personal con una protegida. La regla de las columnas "hermanas" tapaba el minimo, el maximo, la mediana, la media y el desvio de toda columna clasificada con el mismo tipo que una protegida aunque su poder discriminante fuera debil -un importe entero de siete cifras, un numero de factura, ambos documento_identidad debil al lado de la cedula-, con un motivo falso ("La columna se protegio como dato personal") en resumen_tabla$sql, en meta$familias y en cobertura_metricas de la coleccion, mientras perfilar() los publicaba y el mismo objeto tambien, en perfil_muestra. La regla existia porque por esa puerta el piso por valor no veia la tabla entera; desde la ronda 33 la ve, y quedo tapando sin proteger nada. Se retira: la misma tabla publica lo mismo por las dos puertas, y una copia del documento la tapa el piso por valor (medido: la cedula copiada en el medio de una columna de codigos, como su moda, como REAL o escrita de otra forma en un texto, 0 apariciones en SQLite y PostgreSQL, con y sin bloques). - La columna que copia la cedula protegida sin su verificador se protege entera. Una columna de nombre neutro con la cedula de cada fila sin su verificador -el entero 4685467 frente a 4.685.467-9, o el mismo numero como texto- se clasificaba documento_identidad debil y publicaba sus dos extremos, que son dos cedulas, en perfilar(), en perfil_muestra de perfilar_dbi() y en los cuantiles de analizar() y distribucion_valores(). Ahora se clasifica documento_identidad alto ("documentos de cedula sin su verificador: 1.000 de sus valores distintos") y se protege como cualquier columna personal, por las dos puertas y en medir(), si al menos tres de sus valores distintos son el documento sin verificador de un valor protegido (la regla de los textos, .prefijo_documento()), si son el 90 % de sus distintos -el umbral de verificado- y si tantos no coinciden por azar con la densidad de esos documentos en su rango (probabilidad binomial menor que 1e-6). Por columna y no por valor: probar el parcial en el extremo de cualquier columna tapaba el minimo o el maximo de uno de cada cinco importes de siete cifras frente a un millon de cedulas, y en una base de cuatro millones coinciden hasta tres de cada cuatro. Medido en data-raw/medir_tapado_de_mas.R sobre 400 columnas por familia, con un millon y con cuatro millones de cedulas protegidas: 0 % de columnas de importes de siete cifras (tambien redondeados a miles, con 10, 50 o 500 valores distintos, aunque por azar coincidieran todos) y de enteros de ocho cifras; 100 % de las copias con 50 o 500 valores distintos, y con 10 frente a un millon; con el 10 % de tipeos, 100 % y 51,5 %. Limitacion conocida (baja): una copia de pocos valores distintos frente a muchos documentos protegidos no se distingue del azar -3 valores, ninguna; 10 frente a cuatro millones, ninguna- y publica sus extremos; tampoco una copia con mas del 10 % de otros valores. Una columna que coincide por azar publica sus extremos aunque coincidan. - Una palabra de persona en el nombre de la columna ya no protege una clave, una categoria ni una fecha. cliente_id, id_cliente, persona_id, usuario_id, tipo_cliente, estado_socio o cliente_desde se protegian como "nombre" por el token cliente/socio/usuario: perdian la moda, los extremos y los ejemplos, y sus categorias -"mayorista", "empresa"- entraban al piso y se tapaban en columnas que no tenian nada que ver (segmento, un texto libre), por las dos puertas. La ayuda dice que medio es que el nombre "expresa una categoria personal", y tipo_cliente no la expresa. Ahora la palabra de persona clasifica la columna como nombre solo si ninguna otra palabra la clasifico (direccion_cliente es un domicilio), si el resto del nombre no dice que es una clave, un tipo, un estado, una categoria, una fecha, una cantidad o un importe, y si sus valores pueden ser nombres (no numeros, fechas ni logicos). nombre_cliente, socio, cliente o titular con nombres siguen protegidas. Cambia datos_administrativos: id_persona (1 a 13) ya no se protege. Y es la misma propiedad en las demas reglas de nombre, cerrada con la misma regla (.nombre_expresa_tipo(): la palabra del tipo clasifica salvo que el RESTO del nombre la califique, con los calificadores que valen para cada tipo): tipo_documento se protegia como documento con poder alto, fecha_contacto y medio_contacto como telefono, estado_correo como correo y tipo_domicilio como domicilio. Siguen protegidos nro_documento, documento_id, numero_celular, telefono_contacto, barrio_id, segundo_nombre y la fecha de nacimiento guardada como numero (medido: 0 apariciones con cedulas, celulares, correos y nombres reales en esas columnas). - Con la vista antes que su base, otras_tablas no publica el resto de un nombre. perfilar_coleccion(conservar_perfiles = TRUE) volvia a leer los mensajes del motor antes del cruce entre tablas, y el cruce escribia despues en resumen_tabla$meta$proteccion_personal$otras_tablas el motivo de la tabla que no se pudo leer, con el mensaje como se publico al leerla: "De Los Campos" (SQL Server en sueco) o "Campos Cadena" tras un apodo entre comillas (en ingles, y en duckdb). La relectura corre tambien despues del cruce; el orden de las tablas no cambia lo que se publica. - El cierre no borra el nombre de la tabla. Con la lectura fuera de la muestra fallida -o sin muestra, con bloque_muestra = "solo_agregados"-, un valor de una columna no personal igual al nombre de la tabla entraba al cierre y tapaba el nombre en meta$tabla, en cada SQL guardada (FROM "[valor protegido]"), en print() y en perfil_muestra$meta. El nombre de la tabla y el de su esquema son ahora intocables como los de las columnas en todos los pisos de perfilar_dbi() y del cruce de la coleccion, y un valor igual a ellos no entra a un cierre: ya se publica como estructura. Limitacion conocida (baja): un valor igual al nombre de una COLUMNA si se tapa -un nombre de columna puede ser el de una persona-, y con el cierre tapa ese nombre en meta$dialecto$sondas y, por bloques, en meta$fuente_bloques y meta$plan. Las mediciones: - analizar() sin fecha ya no declara la fecha en su perfil: perfil$meta$fecha_declarada era TRUE porque analizar() le pasa siempre la fecha a perfilar(), y comparar_perfiles() exigia el orden temporal entre dos horas de corrida. Dos entregas analizadas fuera de orden abortaban con un error de orden falso, y seguir su consejo -invertir los argumentos- publicaba el deterioro como mejora. Ahora el perfil lo dice igual que la medicion del mismo objeto (refutacion 34-D, M5). - comparar_perfiles() declara el cambio de columnas_opcionales entre las dos corridas con la fila configuracion_columnas_opcionales (severidad error), como ya lo hacia con aplicabilidad: el mismo dato perfilado sin y con la declaracion -la que recomienda posible_ausencia_estructural- salia como mejora de faltantes, y al retirarla como un deterioro con hallazgo faltantes aparecido, sin decir que cambio el universo de completitud. Un perfil anterior a la ronda 33, que no registraba la declaracion, no se compara en ese aspecto (refutacion 34-D, M4). - Un codigo declarado en columnas_codigo y escrito con separador decimal -un rubro "1.1", un "10,5", "$10"- ya no recibe numero_como_texto (ni unidades_mixtas o monedas_mixtas), y planificar_limpieza() no le propone convertir_numero_regional: el plan quedaba lista con aplicar = TRUE, tambien desde analizar(), y aplicar() dejaba la columna numeric. Se declara en cobertura_diagnosticos. La guarda del plan vale tambien para un perfil calculado antes (refutacion 34-D, M6). - Las relaciones aritmeticas entre columnas ya no incluyen las declaradas en columnas_codigo (cod_localidad = cod_depto * 100 salia como regularidad proporcional): se declaran en cobertura_diagnosticos, como Tukey y Benford (refutacion 34-D, B2). - La corroboracion de perfilar_dbi() compara solo las metricas que resumen_tabla y perfil_muestra calculan con la misma semantica, y lo decide una sola funcion: publicaba como divergencia -y perfilar_coleccion() subia la tabla a alcance = "divergencias"- lo que una declaracion o una guarda hacen distinto a proposito: n_faltantes con columnas_opcionales o aplicabilidad, cinco metricas de magnitud con sentinelas_numericos declarados, y n_ceros y n_negativos de un BIGINT desde 2^53, que el resumen no publica. La ronda 33 lo habia cerrado solo para columnas_codigo. Lo que no se compara se publica en meta$corroboracion_bloques$no_comparadas, con su causa. NaN e infinitos se siguen comparando (refutacion 34-D, M1). - La corroboracion tolera en media y desvio -que cada lado acumula con su algoritmo- una diferencia relativa de 1e-10; los valores leidos (minimo, maximo, mediana, moda) siguen en 64 * .Machine$double.eps. El STDEV de SQL Server se apartaba de R hasta 2e-13 relativo sobre enteros de millones, y la corroboracion acusaba a la muestra. Una divergencia real es de otro orden y sigue saliendo. Queda en meta$corroboracion_bloques$tolerancia (refutacion 34-D, M2). - En PostgreSQL, las columnas money y real se leen convertidas en el motor -CAST(x AS NUMERIC) y CAST(CAST(x AS TEXT) AS DOUBLE PRECISION)- en toda lectura, y el resumen SQL calcula sobre esa misma lectura. RPostgres leia 0 en toda celda money, sin avisar: la muestra publicaba constante y filas_duplicadas con severidad error, y con bloque_filas el resumen publicaba minimo, maximo, media y desvio 0 sin ninguna divergencia. Y una columna real daba dos medias segun la via -el motor promediaba el binario y R leia el texto corto- con divergencias falsas. Las columnas convertidas quedan en resumen_tabla$meta$lectura_convertida (refutacion 34-D, M3 y M7). - El motivo de un BIGINT desde 2^53 ya no dice que el controlador lo entrego como doble cuando lo entrego como integer64, ni le recomienda en PostgreSQL, SQL Server o MariaDB una opcion de RSQLite y duckdb: si llego exacto lo dice, y si llego como doble nombra el argumento de cada controlador (refutacion 34-D, B3). - El desvio de perfilar_dbi() ya no pierde cifras cuando la media es muy grande frente al ancho de la columna. STDEV de SQL Server acumula en una sola pasada y publicaba desvio 0 sobre una columna de marcas de tiempo en milisegundos de una ventana de un segundo (desvio real 314,77), sin aviso con una muestra parcial; el desvio nativo de SQLite, duckdb, MariaDB y MySQL se apartaba 4,5e-8, y bloque_filas 8,7e-8. Se pide al motor sobre los valores centrados en la media ya calculada -siempre por columna, y en la consulta compartida otra vez, centrado, cuando la media supera diez veces el desvio-, y bloque_filas acumula sus momentos centrados. Medido en los seis motores: el error queda en 1e-14 y los datos comunes no cambian. Como red, un desvio 0 sobre una columna con un minimo menor que el maximo queda no_disponible con su motivo (refutacion 34-D, segunda vuelta). - Limitacion conocida, documentada en ?perfilar_dbi: un texto hexadecimal ('0x10') en una columna NUMERIC de SQLite pasa la sonda de magnitud como numero (refutacion 34-D, B1). - ?analizar y ?perfiles_madurez dicen que la evaluacion de analizar() usa la medicion agregada, y que sobre medidas por fila de 0 o 1 los tres perfiles de madurez dan la misma cifra (refutacion 34-D, B4). Las colecciones (altas): - relaciones_coleccion(orden = ) ordena la lectura y no la recorta. Sin columnas_candidatas, declarar orden hacia que cada tabla se leyera solo por sus columnas de orden (SELECT id ... ORDER BY id): la comparacion veia una columna por tabla, las demas relaciones desaparecian sin aviso y el objeto se declaraba estable = TRUE. Medido con SQLite: 0 relaciones de 2 siguiendo el como_resolverlo del propio paquete, que pide declarar orden. Ahora una tabla sin candidatas se lee entera, con su ORDER BY, y a una con candidatas se le suman las de orden solo para ordenar. La comparacion toma las candidatas de la misma busqueda que la lectura. - coleccion() reconoce el controlador por la clase de la conexion y no carga uno que el usuario no uso. Una conexion de RMariaDB::MariaDB() a un servidor MySQL informa "mysql", y coleccion() pedia RMySQL: sin RMySQL se negaba, y con RMySQL lo cargaba; las dos clases se llaman MySQLConnection, el metodo de RMySQL tomaba el despacho y la conexion del usuario quedaba inservible (perfilar_coleccion() y la consulta siguiente del usuario daban error; relaciones_coleccion(), 0 relaciones). Ahora el paquete pedido es el que define la clase de la conexion -o el de una clase de la que hereda-; el motor que informa el servidor solo elige la fila del catalogo. Lo mismo vale para odbc contra MySQL o PostgreSQL (no medido: no hay esos controladores ODBC en la maquina de pruebas). Una sesion que no puede escribir un nombre (alta, la propiedad de la ronda 33 en otro sitio): - Un nombre de tabla marcado latin1 ya no deja rota la conexion de odbc. perfilar_dbi() y plan_perfilado_dbi() llamaban a dbExistsTable() con el nombre crudo antes de la guarda de la ronda 33; odbc lo pasa al controlador sin convertirlo, y con el nombre de read.csv(encoding = "latin1") -o esos bytes sin marca- la conexion quedaba rota: todo lo siguiente fallaba y la segunda consulta del usuario hacia caer a R (SQL Server por FreeTDS, sesion UTF-8). Bajo C, dbQuoteIdentifier() escapaba el nombre a texto ASCII que la guarda dejaba pasar: otro identificador. Ahora todo nombre que va a la conexion se normaliza una vez -lo marcado latin1 pasa a UTF-8; lo UTF-8 valido se marca- y pasa por la guarda antes de la primera llamada, tambien dbExistsTable() y dbListFields(), que salen por dos envoltorios que la miran (una prueba recorre el espacio de nombres). perfilar_dbi(), plan_perfilado_dbi() y coleccion() -por texto, data.frame o DBI::Id- leen el nombre latin1; uno con bytes que no son UTF-8 valido y no declaran codificacion da lupa_error_codificacion_dbi antes de tocar la conexion, con el remedio de leerlo con fileEncoding (correr con un locale UTF-8 no lo arregla). La guarda bajo un locale UTF-8 ya no deja pasar esos bytes, y coleccion() ya no convierte un nombre asi en el texto "NA". Las colecciones bajo C (media y bajas): - relaciones_coleccion() publica los nombres de columna de dbListFields(). Bajo C, FreeTDS devuelve los nombres de SELECT * en latin1 (a\xf1o) y dbListFields() en UTF-8: relaciones_coleccion() publicaba los primeros, que no aparean con los de perfilar_coleccion() de la misma sesion, devueltos como columnas_candidatas daban error y en una sesion UTF-8 hacian abortar nchar() y toupper(). Los nombres en bytes no UTF-8 se toman ahora de la fuente del paquete, por posicion y solo si los demas coinciden. - Una columna que la sesion no puede escribir no se lleva el par. Bajo C, una candidata o un orden no ASCII por odbc hacia fallar la lectura de la tabla entera -se perdia tambien id -> persona_id- con un motivo tapado dos veces y un remedio de permisos. Ahora la candidata sale de la lectura y sus combinaciones van a cobertura_podas con motivo columna_no_escribible; un orden asi deja la lectura sin ORDER BY, y meta$lecturas y meta$estable = FALSE lo dicen. El motivo de una lectura fallida se publica una sola vez, legible, con el remedio de la codificacion cuando corresponde. - Un orden con bytes sin codificacion declarada no aborta. Bajo una sesion UTF-8, relaciones_coleccion(orden = ) con los bytes de un CSV latin1 leido sin fileEncoding daba un error de R desde dbQuoteIdentifier() de RSQLite; ahora la lectura va sin ORDER BY y lo dice. Las bajas de la misma construccion: - analizar() lee los argumentos de perfilar() con su nombre completo. Con argumentos_perfil = list(aplic = ...) el perfil usaba el universo declarado y el tablero no: NoNulo 0,27 en vez de 0,9 sobre 300 filas aplicables de 1000, sin aviso. Es la regla de la ronda 33 (.argumentos_para_perfilar()), que faltaba en esta via. - Un prefijo de varios argumentos por ... dice que es ambiguo. n = TRUE en perfilar_por(), perfilar_dbi() o perfilar_coleccion() decia "n no es un argumento de perfilar()"; ahora nombra los que casan (nombre, normalizar). - Los mensajes del paquete ya no escriben character(0). Un argumento NULL -un if sin else- salia escrito en el texto ("la medida xcharacter(0)"); se descarta como en stop() de base. Limitacion conocida: - perfilar_coleccion() valida al entrar lo que va a perfilar(), no los argumentos propios de perfilar_dbi() (estrategia_distintos, max_consultas, umbral_cardinalidad...): uno mal escrito deja cada tabla en cobertura_coleccion con ese motivo, sin aviso. Documentado en ?perfilar_coleccion. Ronda 33: la primera ronda con el porton por severidad Desde esta ronda la etapa de refutacion cierra cuando una ronda no encuentra nada alto ni medio; lo bajo se documenta. Tres refutaciones sobre la ronda 32 y sus cuatro decisiones: la de la proteccion encontro uno alto -por la via por bloques el piso por valor no corria sobre las columnas no personales: las cedulas y los nombres que repetia otra columna se publicaban- y dos medios en las colecciones; la de las decisiones, cuatro medios -la NoNulo de una columna opcional, divergencias falsas con columnas_codigo, la evaluacion de analizar() sin su configuracion, el centinela con coma decimal-; la de las piezas compartidas, uno medio -con LC_NUMERIC en coma se perdia la fraccion de segundo-. Al arreglarlos aparecieron dos mas: la media de una columna entera salia recortada en SQL Server, MySQL y MariaDB -y la corroboracion publicaba una divergencia falsa en cada una-, y bajo el locale C una columna con tilde en SQL Server hacia caer a R. La proteccion: - Por la via por bloques corre el mismo piso por valor que sin ella, sobre todas las columnas. Con bloque_filas, perfilar_dbi() protegia el resumen solo con los extremos de las columnas protegidas: no usaba la muestra ni leia los valores de la tabla entera, y una cedula copiada en una columna de codigos o un nombre citado en un texto libre se publicaban en resumen_tabla$columnas$maximo, en el motivo de la corroboracion -que ademas declaraba una divergencia que no existia: la proteccion tapaba un lado y no el otro- y en meta$familias (distintos, cuantitativos), en SQLite y PostgreSQL y en perfilar_coleccion(conservar_perfiles = TRUE). Medido: 96 apariciones, 14 de 16 nombres y 40 de 120 cedulas en una tabla de 120 filas; sin bloques, 0. La ronda 32 habia cerrado meta$familias solo en las columnas protegidas. Ahora las dos vias llaman a la misma funcion (.proteger_salida_muestra_dbi(): la muestra, los rellenos confirmados, la lectura fuera de la muestra o el cierre del lado seguro) y la misma tabla publica lo mismo con y sin bloque_filas. Lo que el piso tapa en columnas -un extremo que es un documento, y sus derivados: la media y el desvio que lo reconstruyen- se tapa tambien en meta$familias$$cuantitativos; y si la lectura fuera de la muestra falla, el cierre tapa tambien los valores distintos de las columnas no personales en meta$familias. Cuesta lo que la via sin bloques: un perfilar_dbi(bloque_filas = 10) protegido de 24 filas paso de 1,6 s a 1,9 s. - El piso final de perfilar_coleccion() lee cada mensaje del motor entero, en cualquier orden de las tablas. El mensaje pasa por el piso al armarlo, con los valores que se conocen en ese momento; con una vista leida antes que su tabla base el piso no conocia el nombre, la segunda pasada omitia lo que no es una palabra permitida y dejaba su RESTO -"De Los Campos" de "Maria De Los Campos" en SQL Server en sueco, "Campos Cadena" despues de un apodo entre comillas en ingles- en los tres campos de motivos, en el perfil retenido de la vista, en el aviso y en print(), con y sin perfiles: la huella es la del valor entero y el resto no la tiene. Ahora la coleccion guarda cada mensaje como llego -en memoria, mientras dura; nunca entra al objeto- con los nombres que estaban permitidos, y al final lo vuelve a leer con todo lo que leyo y cambia lo publicado (.repasar_mensajes_motor()), solo si lo nuevo publica menos. El diagnostico queda: "Conversion failed when converting the nvarchar value '[literal omitido]' to data type int". Limitacion conocida (baja, no medida): la relectura reemplaza el texto tal como se publico; si el piso de la propia tabla ya lo habia cambiado -el mensaje citaba entero, ademas, un valor protegido de esa misma tabla-, no lo encuentra y queda solo el piso de las huellas, que tapa los valores enteros y no el resto. - Con perfiles retenidos el piso final usa tambien las huellas de lo leido. Usaba solo los valores de las tablas completas: con una tabla base incompleta -una columna protegida ntext de SQL Server no admite el GROUP BY que lee sus valores- el motivo de la vista leida antes citaba el valor ENTERO, en siete lugares, y sin perfiles se tapaba: retener perfiles protegia menos. Ahora el piso de huellas corre con y sin perfiles en los tres campos y los avisos, y sobre los perfiles retenidos (.proteger_perfiles_con_mensajes_motor()) con los valores que citan los mensajes. - El mensaje del motor se guarda sin los colores de cli. En una consola a color -RStudio, cli.num_colors = 256- cli formatea las partes del error de odbc al senalarlo, y resumen_tabla$sql$motivo guardaba las secuencias ANSI (\033[32m[FreeTDS][SQL Server]\033[39m) y el enlace de {.file} con la ruta del directorio de trabajo, con y sin proteccion; con proteccion la segunda pasada las mezclaba con marcadores ([[literal omitido][FreeTDS]). Se quitan en la misma lectura que quita las vinetas (.sin_ansi_motor(), cli::ansi_strip()). Limitacion conocida (baja): lo que cli agrega dentro de una linea no se distingue del texto del motor; sin color cita entre comillas el archivo del controlador ('nanodbc/nanodbc.cpp:1802') y con proteccion esa cita sale [literal omitido], con color se publica. No es un valor de la tabla. Las decisiones de la ronda 32: - La NoNulo de una columna opcional se mide sobre sus celdas presentes. La decision D6 propone y mide la NoNulo de una columna con faltantes disfrazados, pero no trasladaba columnas_opcionales: sobre cuarenta celdas con un NA y tres -999 en una columna declarada opcional, el perfil contaba 3 de 39 (1 - prop_faltantes_totales = 0,923) y medir() y el tablero de analizar() publicaban 0,900 = 36/40, contando como faltante el NA que la declaracion legitima, con la justificacion de la sugerencia diciendo 3 ausentes. Ahora el perfil registra la declaracion en meta$columnas_opcionales, NoNulo acepta opcional = TRUE -mide sobre las celdas presentes y solo cuenta los valores_nulos- y proponer_modelo() se lo pone a la NoNulo de una columna declarada opcional, asi que vale igual por medir() directo, por analizar() y sobre la entrega siguiente. Es una propiedad y no una formula aplicable = ~ !is.na(col) porque la formula nombra la columna como simbolo: bajo LC_CTYPE = "C", con un nombre no ASCII, avisaba "unable to translate" seis veces por medicion. - perfilar_dbi() no corrobora lo que columnas_codigo manda no calcular. La corroboracion entre resumen_tabla y perfil_muestra corria antes de que la declaracion borrara la media, la mediana y el desvio del resumen SQL, y comparaba esa cifra con el NA que la muestra callo a proposito: tres divergencia falsas por columna declarada, con la media del codigo en el texto, por el camino SQL (duckdb y SQLite) y por bloques; con una muestra parcial, ademas, el lado que falta. perfilar_coleccion() las subia a alcance = "divergencias" y pedia releer la tabla. Ahora esas tres metricas de una columna declarada no se corroboran; la fila no_aplica de resumen_tabla sigue declarando la omision. - La evaluacion de analizar() conserva la configuracion del modelo, la aplicabilidad y la fecha declarada. analizar() evalua la medicion agregada del tablero, que no llevaba esos atributos de medir(); evaluar() los copia de lo que recibe, asi que la evaluacion llegaba al historico sin ellos: la deriva no separaba un cambio del conjunto de metricas -publicaba error de 0,5 a 0,333 donde la puerta medir() -> evaluar() declara configuracion_modelo / no_comparable- ni dos tablas -identidad_tabla , y encadenaba personas con visitas como una serie-. Ahora la medicion de analizar() lleva lo que agregar() traslada, con una sola funcion para las tres puertas (.trasladar_atributos_medicion()); por el camino que mide metrica por metrica, la configuracion es la del modelo entero. fecha_declarada dice si la fecha la paso quien llamo a analizar(). - Un centinela escrito con la convencion decimal de la columna es un faltante disfrazado. En una columna de texto con coma decimal, "-999,00" se resumia como -999 (minimo = -999) pero no se contaba como disfrazado, y la columna se quedaba sin hallazgo y sin NoNulo, al lado de la misma cifra escrita "-999.00", que si contaba. El detector, la mascara con que proponer_modelo() arma los valores_nulos y la accion convertir_sentinelas_numericos del plan leen ahora el texto con la convencion que el perfil publica en numero_texto_convencion, como el resumen; la accion la lleva en parametros$convencion. Sobre 60 importes y tres "-999,00": tres disfrazados, NoNulo 60/63 = 0,952 y aplicar() convierte las tres celdas. Tambien cuentan "-999,00" y "-9.999,00" en una columna con miles. - columnas_codigo llega a todo analizar(). clasificar_variables() con el perfil propone nominal, rol categoria y confirmada para una columna declarada -antes discreta y medida-, y analizar() le pasa a detectar_asociaciones() el codigo como categoria: eta cuadrado o V de Cramer, no un Pearson que supone una magnitud. Y un codigo guardado como texto ("006") ya no recibe tipo_declarado_distinto, que pedia convertirlo a numero. - Limitacion conocida, documentada: en perfilar_coleccion() las declaraciones por columna valen para todas las tablas. coleccion() no tiene declaraciones por tabla y ... llega igual a cada perfilar_dbi(): una tabla sin la columna declarada en columnas_codigo no se perfila (sale de resumen_coleccion, con un como_resolverlo que habla de permisos), y con columnas_opcionales o columnas_personales pierde la muestra. Para declarar columnas que no estan en todas las tablas, perfilarlas por separado o partir la coleccion. - La media de SQL Server y el desvio de PostgreSQL se piden al motor como doble. perfilar_dbi() pedia la media como AVG(x * 1.0), que en SQL Server es AVG de un numeric y devuelve seis decimales: sobre 61 enteros el resumen publicaba 16.934426 y la muestra 16.934426229508198, y la corroboracion declaraba divergencia en la media de toda columna entera -tambien bigint- de una tabla leida entera. PostgreSQL hacia lo mismo con el desvio, STDDEV_SAMP(x * 1.0), sobre magnitudes grandes: 8767203.0408 contra 8767203.0408313069. Ahora la media se pide como AVG(CAST(x AS FLOAT)) en T-SQL (SQL Server y Sybase) y el desvio como STDDEV_SAMP(CAST(x AS DOUBLE PRECISION)) en PostgreSQL, que es la cifra que lee R; no se agrando ninguna tolerancia. En SQLite, duckdb y PostgreSQL la media sale igual que antes, cifra por cifra, y el desvio tambien salvo el de PostgreSQL sobre enteros grandes, que ahora es el de R. - La media de MySQL y MariaDB tambien se pide como doble. Ahi AVG(x * 1.0) es AVG de un DECIMAL, que suma div_precision_increment (4) decimales a la escala: la media de una columna int salia decimal(16,5), y el resumen publicaba 16.93443 donde la media es 16.934426229508198, con dos divergencias falsas en la corroboracion (MariaDB 11.8 y MySQL 8.4). Ahora se pide como AVG(x * 1e0): 1e0 es un literal DOUBLE en cualquier version, y CAST(x AS DOUBLE) solo existe en MySQL desde la 8.0.17. STDDEV_SAMP ya devuelve DOUBLE en los dos y no cambia. SQLite, duckdb, PostgreSQL y SQL Server dan lo mismo que con el arreglo anterior. Una sesion que no puede escribir un nombre (alta): - Bajo el locale C, perfilar_dbi() ya no hace caer a R con una columna con tilde en SQL Server. odbc pasa el texto de la consulta por la codificacion nativa de la sesion, y bajo C -la de un Rscript de cron en Linux- es ASCII: con una columna ano con tilde, la lectura del esquema mandaba un texto que no se podia convertir, odbc mandaba NA y dejaba la conexion rota, y la consulta siguiente -la de la primera columna- terminaba la sesion con una violacion de segmento (medido con FreeTDS; con el nombre bien marcado odbc lo escapa y el motor lee otro identificador). Ninguna consulta que la sesion no pueda escribir se manda: toda consulta de la via sale por un solo envio que lo mira antes de tocar la conexion. La columna queda en no_disponible con ese motivo y "Correr la sesion con un locale UTF-8", las demas se leen enteras, y una tabla con un nombre asi da lupa_error_codificacion_dbi, clase nueva. RPostgres, RSQLite y duckdb reciben la consulta en UTF-8 y leen esos nombres en cualquier locale: no cambian. - La via por bloques declara las columnas que el esquema no pudo leer. Con bloque_filas, una columna que el motor rechazaba -o, ahora, que la sesion no puede nombrar- desaparecia de resumen_tabla$columnas, de sql y de la cobertura sin decirlo; ahora sale como en la via SQL, sin medir y con su motivo. La sesion: - Un texto con fraccion de segundo se lee entero con LC_NUMERIC en coma. strptime() lee la fraccion de %OS con la marca decimal del locale, y con Sys.setlocale("LC_NUMERIC", ...) en coma "10:00:05.25" se leia como 10:00:05, sin aviso: un plan armado en una sesion "C" salia lista y aplicar() en una sesion con coma escribia 7 instantes distintos de 12; en la misma sesion planificar_limpieza() bloqueaba la conversion como "no inyectiva", y una fecha de texto con fraccion se guardaba al segundo en perfilar(), medir(), analizar(), perfilar_por(), medicion_desde_estimaciones() y reportar(), y en el historico. Toda lectura de un texto a fecha y hora pasa ahora por un solo lector, que fija LC_NUMERIC en "C" mientras lee y lo devuelve como estaba; llegan a el 31 exportadas. El id_medicion por omision de medir() se escribe con punto (T100005.250000, antes T100005,250000 con coma). - Bajo el locale C, evaluar(), aplicar(), perfilar_dbi(bloque_filas = ) y relaciones_coleccion() ya no avisan "unable to translate" con nombres no ASCII. Es la construccion que la ronda 32 cerro en medir(): unian partes con do.call() sobre una lista nombrada por los perfiles, las reglas o las columnas, y do.call() traduce al nativo los nombres de sus argumentos; con warn = 2 abortaban. relaciones_coleccion() guardaba cada tabla leida en un cache con su nombre como clave, que assign() tambien traduce: bajo C la tabla pais con tilde en la i y una tabla llamada literalmente pas compartian la entrada. Un recorrido de 47 llamadas de exportadas bajo C con nombres y valores no ASCII da ahora cero avisos (antes 24, en esas cuatro). Limitacion conocida, declarada: - Una corrida guardada por una version anterior cuya configuracion lleva un numero que con 15 cifras no vuelve a su valor -maximo = 1/3, 0.1 + 0.2, un umbral calculado con quantile()- declara una vez una fila configuracion_modelo con severidad error contra la primera corrida nueva: la version anterior lo escribia con 15 cifras (0.333333333333333) y la ronda 32 lo escribe con las que lo devuelven a su valor (0.3333333333333333). Es la misma excepcion que ya tenia un instante con fraccion de segundo. Hacerlas aparear exigiria comparar con 15 cifras, lo que la ronda 32 cerro. Lo dice ?detectar_deriva_calidad. La via DBI: - Los errores de argumento de perfilar_dbi() llevan la clase que promete su ayuda. ?perfilar_dbi dice que todo stop() de la via hereda de lupa_error_dbi, y nueve argumentos -universo, muestra, estrategia_distintos, estrategia_mediana, bloque_muestra, dialecto, incluir_valores, max_celdas_muestra, max_bytes_muestra- y la bandera de proteccion por ... daban un simpleError: los valida un ayudante comun a todo el paquete. Ahora llevan lupa_error_argumento_dbi con el mismo mensaje. incluir_valores no se validaba: con NA abortaba con el error de R "valor ausente donde TRUE/FALSE es necesario"; ahora es TRUE o FALSE. La bandera y los argumentos que viajan por ...: - Un nombre abreviado por ... vale lo mismo para todos los que lo leen. perfilar_dbi(), perfilar_coleccion() y perfilar_por() leian la lista de ... por el nombre exacto, y R completa el nombre al llamar a perfilar(): perfilar_dbi(proteger = FALSE) perfilaba la muestra sin proteccion y tapaba la moda del resumen SQL, proteger = "si" o NA devolvia el fallo parcial con el nombre del argumento tapado en vez del error del paquete, y perfilar_por() protegia sus etiquetas de grupo mientras cada grupo se perfilaba sin proteccion (y leia asi otros seis argumentos). Los nombres se completan ahora en la entrada con la regla de R, contra los formales de perfilar(). - Lo que viaja por ... hasta perfilar() se valida al entrar, aunque no haya nada que perfilar. La ronda 32 lo hizo con la bandera; seguian pasando en silencio, sin grupos que llegaran a min_filas o con bloque_muestra = "solo_agregados", una fecha invalida ("ayer", NA, dos fechas) y un nombre que perfilar() no tiene (no_existe = 1, una errata). perfilar_por(), perfilar_dbi() y perfilar_coleccion() dan ahora el error del paquete; en perfilar_coleccion() una vez, en la entrada, y no tabla por tabla (que habria perdido los resumenes SQL). Limitacion declarada: los demas valores los valida perfilar() al perfilar, y sin nada que perfilar no se validan. - Con la proteccion activa, el motivo de una muestra que no se pudo perfilar por un argumento sale legible. El error de perfilar() pasaba por el piso del mensaje del motor y decia "[literal omitido] debe ser un entero [literal omitido] de al [literal omitido] 2.": no se sabia que argumento fallo. Si perfilar() da el mismo error sobre la muestra sin filas, el error no cita ninguna celda y se trata como una condicion propia de la via: se tapa solo lo citado entre comillas que no es un nombre de la tabla o de sus columnas. Si no se repite, el piso entero, como antes. Ronda 32: las entradas, la sesion otra vez, la proteccion y cuatro decisiones Tres refutaciones y una vuelta de decisiones. La de las entradas encontro once hallazgos -dos funciones que apagaban la proteccion con una bandera mal escrita, una fecha de texto leida en el huso de la sesion, un nombre de columna largo que abortaba, unos treinta argumentos sin validar-; la de la sesion, nueve -digits.secs decidia si una conversion era reversible, y la configuracion que se compara entre corridas cambiaba entre un Rscript de cron y RStudio- y uno mas al arreglarlos: con LC_NUMERIC en coma los controladores de PostgreSQL y SQL Server leian 2.5 como 2; la de la proteccion, seis -un numero partido en lineas en el mensaje del motor- y uno mas, alto, al arreglarlos: la lectura por bloques publicaba los valores de las columnas protegidas. Y cuatro decisiones: las columnas de codigo se declaran, el id de una sugerencia del modelo se arma con lo que mide, la NoNulo con faltantes disfrazados se mide, y el paquete ya no tapa stop() ni warning() de base. Despues, sin proteccion, el mensaje del motor se publica entero. La proteccion: - El mensaje del motor se lee como lo dijo el motor, sin lo que agrega cli. odbc arma su error con cli::cli_abort(): guarda cada linea del mensaje del motor como un elemento y conditionMessage() las une con una vineta (\n y U+2022), junta los blancos y corta las lineas al ancho de la consola. Un numero partido en dos lineas -2901\n1234, que la regla de digitos lee como uno de ocho cifras- llegaba partido por la vineta, que la regla lee como un punto medio que no une: con SQL Server en sueco o neerlandes, perfilar_dbi() publicaba en resumen_tabla$sql$motivo el fijo, la cedula 4.685\n467-9 o el celular 099\n123\n456, y perfilar_coleccion() lo publicaba en sus motivos aunque habia leido la base, con y sin perfiles; y una consola de 48 a 52 columnas cortaba 2901 1234 en dos lineas. Ahora el texto de una condicion de rlang se arma con sus partes (message, body, footer, las de la condicion que la causo) unidas por el salto que puso el motor, y asi se publica, sin vinetas (.texto_condicion_motor(), la entrada de .mensaje_motor()). duckdb, RPostgres y RSQLite dan el mismo texto que antes. - La seleccion de los valores del motor lee el valor con la misma regla que la segunda pasada. La unica regla de la segunda pasada que mira lo que rodea al valor -la linea que el motor repite sobre un ^- omitia entero, leido solo, el valor que termina en \n^, y la coleccion no lo guardaba; dentro del mensaje la linea del ^ sigue con el texto del motor, la regla no corre, y 099;123;456\n^ o "Florencia Martinez\n^" se publicaban. La seleccion pregunta ahora con la lectura del peor contexto (toda_palabra = TRUE), que no aplica esa regla: solo puede guardar de mas. - El celular escrito como numero de planilla con punto medio o con el agrupado indio se lee como la misma cifra. El separador de miles del numero de planilla era una lista propia -punto, coma, apostrofo y espacio-: con el punto medio (U+00B7, y el operador vineta y el punto medio katakana, que el piso lleva a el) o con el agrupado indio (59,89,52,71,823), el celular E.164 de una nota salia en claro en 60 de 60 notas de la copia de analizar() y en los ejemplos del perfil. Ahora el separador es la clase con que el piso une los grupos de un numero compacto, mas el espacio, y el agrupado indio es una forma del mismo numero, en la lectura de la cifra y en la del texto (una sola pieza). - Los avisos de una coleccion que se corta pasan por el mismo piso. Si perfilar_coleccion() se interrumpia -Ctrl+C, un error- despues de leer una tabla y antes del piso final, los avisos juntados salian como llegaron, y el de una vista leida antes que su tabla base citaba el celular protegido. El piso final es ahora una sola funcion que aplica tambien el on.exit(), con los valores que se conocian; si falla, el texto del aviso no se publica. - La copia de analizar(conservar_datos = TRUE) mira los atributos de un entorno y cierra lo que no puede leer. Un entorno con la cedula en su atributo name -que print() muestra- o en otro, o el de una funcion con ese atributo, quedaba entero en la copia; y uno con una promesa o una ligadura activa que aborta al leerse se devolvia como estaba, con la cedula en otra ligadura. Ahora los atributos pasan por el piso -sin modificarlos: el entorno es del usuario-, y lo que no se lee lleva el marcador, como una funcion que no se puede escribir. - perfilar_dbi(bloque_filas = ) no publica los valores de las columnas protegidas en meta$familias. La via por bloques guarda en resumen_tabla$meta$familias lo que devolvio cada acumulador, y salia entero: en distintos, cada valor distinto de las columnas protegidas -todas las cedulas y los nombres de la tabla, en SQLite-, y en cuantitativos los extremos y el desvio de una protegida de valores cortos, que el piso por valor no busca. Ahora se tapan con la misma lista de campos que la tabla de columnas, en las mismas columnas. Encontrado al medir el desvio por todas las vias. - El desvio de una columna protegida deja de publicarse. Era la unica excepcion declarada -la decision F-6: "una dispersion no identifica a nadie"- y la decision se revirtio: el desvio es una magnitud de los valores de la columna. Se tapa como la media en perfilar(), perfilar_dbi() (con o sin bloques; su fila de resumen_tabla$sql dice omitido_por_privacidad) y perfilar_coleccion(), con una sola lista (.CAMPOS_MOMENTO_PROTEGIDOS), y detalle_proteccion_personal dice [estadisticos de orden y momentos protegidos] cuando tapa los dos momentos. - La coleccion guarda huellas de los valores protegidos, no sus textos. Para el mensaje del motor de las demas tablas guardaba el texto de todo valor con letras que leia, y la memoria crecia con los nombres. Ahora guarda por valor una huella de 52 bits de su forma plegada y sin separadores (ocho bytes, base R), y el mensaje se compara tramo por tramo: un tramo con la huella de un valor guardado se tapa como esta escrito. Dos formas con la misma huella, una en 2^52, solo tapan de mas. Medido con aud31-07 (SQLite, conservar_perfiles = FALSE, a la par con la ronda 31): sobre 800.000 filas con nombres el indice pasa de 73,2 a 6,1 MB, el tiempo de 324,8 a 310,2 s y el RSS de 875 a 765 MB; sobre 1.600.000, de 146,5 a 12,2 MB, de 649 a 610 s y de 1849 a 1726 MB. - La bandera proteger_datos_personales se valida en un solo lugar, con un solo mensaje. detectar_discordancias() y descubrir_patrones() la leian con isTRUE(): NA, NULL, "si", "TRUE" -el texto que deja un archivo de configuracion-, c(TRUE, TRUE) o -1 apagaban la proteccion y publicaban el correo crudo en evidencia y en los ejemplos, mientras las otras once funciones rechazaban esos valores (dos con otro mensaje). Por ... pasaba lo mismo: perfilar_dbi() calculaba y guardaba en el resumen SQL el valor que con TRUE declara omitido por privacidad, perfilar_coleccion() lo heredaba y perfilar_por() aceptaba NA en silencio cuando ningun grupo llegaba a min_filas. Ahora las trece funciones con la bandera y las tres que la reciben por ... dan el mismo error ("proteger_datos_personales debe ser TRUE o FALSE."), y una prueba recorre las funciones exportadas con ese argumento: una nueva tiene que entrar en la lista. La fecha y la entrada: - medicion_desde_estimaciones() lee la fecha como medir(), cuyo contrato promete. Leia un texto con as.POSIXct() en el huso de la sesion: la misma llamada con fecha = "2026-03-01" guardaba las 00:00, las 03:00 o las 15:00 del dia anterior en UTC segun la maquina, con el mismo id_medicion, y el historico la ordenaba despues de una medicion propia de la 01:00. Aceptaba ademas NA y dos fechas, y abortaba con un error de R con NULL, TRUE o un texto ilegible. La regla de la fecha de una funcion exportada queda escrita una vez -un instante, leido en UTC, o el error del paquete- y la usan perfilar(), reportar(), medir(), analizar() y medicion_desde_estimaciones(). medir(fecha = TRUE) da ahora el error del paquete, y medir(fecha = "ayer") el mismo mensaje que perfilar(). - analizar() acepta la fecha de texto que aceptan perfilar() y medir(). analizar(d, fecha = "2026-03-01") abortaba con "argumento 'trim' invalido": el texto crudo llegaba al armado del id. Ahora la fecha se lee una vez, en la entrada. - Un nombre de columna de mas de 2.500 bytes ya no aborta. El conjunto de nombres usados se guardaba en un entorno con la clave por bytes como nombre de variable -hasta cuatro caracteres por byte-, y R no admite uno de mas de 10.000 bytes: perfilar() y otras cuatro abortaban con "nombres de variables se limitan a 10000 bytes". El caso real es un CSV leido con el separador equivocado, cuya unica columna se llama como la cabecera entera. La pertenencia se sigue decidiendo por la clave entera, asi que el resultado no cambia para nombres cortos. medir() tenia el mismo limite por otro lado -nombraba los argumentos de do.call(rbind) con la metrica instanciada, que lleva el nombre de la columna- y tambien se arreglo. - inferir_tipo() rechaza una tabla como sus hermanas. tibble[, "f"], data.table[, "f"] y grouped_df[, "f"] dan una tabla de una columna, y inferir_tipo() contestaba el nombre de su clase -tipo = "tbl_df"- donde el vector daba texto. Ahora da el error que ya daba detectar_formatos_fecha(), escrito una vez para las dos. - Inf en un tope entero da el error del paquete. sugerir_clave(maximo), elegir_clave(maximo), detectar_discordancias(max_ejemplos), perfilar(max_columnas_orden, max_columnas_aritmetica) y perfilar_por(min_filas) pasaban la guarda con Inf y abortaban con un aviso y un error de R. Como los topes hermanos de una busqueda cara, rechazan Inf con un mensaje que nombra el argumento; los topes de lo que se muestra (max_patrones, max_filas_hallazgo, muestra) siguen admitiendolo. - NULL, largo cero, NA o el tipo equivocado dan el error del paquete en unos treinta argumentos. Las guardas que validaban varios umbrales juntos los unian con c(), que borra un NULL, aplana dos valores y convierte TRUE en 1: el valor malo pasaba y abortaba despues en if, head() o formatC(). Ahora cada umbral tiene que ser un numero antes de unirlos, y los topes enteros se validan uno por uno con un mensaje que nombra el argumento ("max_columnas debe ser un entero positivo." en lugar de "Los limites deben ser enteros positivos."). Incluye perfilar(nombre) -con una funcion construia el perfil y su print() abortaba-, metrica(dimension, factor), reportar(archivo), comparar_perfiles() y descubrir_patrones(na.rm, distinguir_mayusculas, expandir), que con NA o "si" se leian como FALSE en silencio. - agregar(pesos) con otra funcion que promedio_ponderado se rechaza, como umbral fuera de ratio_umbral. Con ratio los pesos no pesaban y se publicaban igual en pesos_declarados, y partes_con_peso_cero declaraba partes que si habian entrado; con otro largo abortaba con un error de R. - Las opciones de texto dan un error que nombra el argumento. Diez argumentos exportados usaban match.arg() crudo: con NA o un numero decia "'arg' must be NULL or a character vector". Ahora "funcion debe ser 'ratio', 'ratio_umbral', 'promedio' o 'promedio_ponderado'.", con el mismo resultado que match.arg() sobre todo lo que este acepta. Tambien en perfilar_dbi() y perfilar_coleccion(). - aplicar() devuelve la data.table con su espacio reservado. Las acciones asignan con [[<-.data.frame, que copia la tabla sin la sobreasignacion: volvia con truelength 0 y el primer := del usuario avisaba "Se tomo una copia superficial". La misma respuesta en cualquier sesion: - Una conversion a fecha-hora con fraccion de segundo no depende de options(digits.secs). El plan comparaba el instante convertido con format() sin formato, que toma los decimales de esa opcion: con el valor por omision dos instantes del mismo segundo salian iguales y planificar_limpieza() bloqueaba como "no inyectiva" -10 valores de 12- una conversion que conserva cada valor; con digits.secs = 6 la dejaba lista y aplicar() la escribia. El instante se compara con todas sus cifras, en UTC, en cualquier sesion. - La configuracion que se compara entre corridas no depende de LC_NUMERIC, del locale C ni de digits.secs. deparse() escribe la marca decimal de LC_NUMERIC (x > 0,9) y bajo C escapa el texto no ASCII ("S"), y la configuracion guardaba los instantes con format() sin formato: una corrida de cron contra una de RStudio, o una sesion con Sys.setlocale("LC_NUMERIC", ...), hacian que comparar_evaluaciones() y detectar_deriva_calidad() afirmaran un cambio de perfil, de modelo y de aplicabilidad que no hubo. Todo deparse() del paquete escribe ahora con LC_NUMERIC en "C" y, bajo C, con un LC_CTYPE UTF-8 mientras escribe: sale lo mismo que en una sesion UTF-8, con los mismos cortes de linea. Lo mismo arregla el texto publicado de la regla de aplicabilidad y el nombre de los datos (que bajo C salia datos). Una evaluacion guardada antes en una sesion UTF-8 sigue apareando; la excepcion es un instante con fraccion de segundo en la configuracion (vigencia(fecha_acceso = )), que antes se guardaba cortado al segundo y ahora con su fraccion: esa corrida declara el cambio una vez. - Lo que se lee de una base de datos no depende de LC_NUMERIC. Los controladores convierten el texto que devuelve el motor con la marca decimal del locale: con Sys.setlocale("LC_NUMERIC", ...) en coma, RPostgres leia 2.5 como 2 (y odbc, un decimal de SQL Server), y perfilar_dbi() publicaba sin aviso media 3, mediana 2, minimo 0 y maximo 10 sobre una columna de media 3.7, mediana 2.5, minimo 0.5 y maximo 10.25. coleccion(), perfilar_dbi(), plan_perfilado_dbi(), perfilar_coleccion(), estimar_costo_coleccion() y relaciones_coleccion() leen ahora con LC_NUMERIC en "C" y lo devuelven como estaba al terminar, tambien si terminan en error. - El porcentaje de TABLESAMPLE se escribe con punto. formatC() tomaba la marca de OutDec y el sprintf() de C la de LC_NUMERIC: con cualquiera de las dos en coma la sentencia decia TABLESAMPLE SYSTEM (12,5), el motor la rechazaba y perfilar_dbi(universo = "muestra_motor") se quedaba sin muestra. - distribucion_valores() agrupa una duracion por su valor y la escribe como un doble. Una columna difftime se publicaba con as.character(): la etiqueta seguia a OutDec, LC_NUMERIC y scipen (1,5, 1.5e+00), 0.1 + 0.2 y 0.3 caian en una sola fila y aparecia una fila NA con frecuencia 0. - Dos cifras que la ronda 31 no alcanzo ya no salen en notacion cientifica con scipen negativo: el motivo de perfilar_coleccion() ("No se calcularon 14 de 105 agregados") y la fuente de la proyeccion de la mediana de perfilar_dbi() ("68 ms por millon de filas"). - Con LC_NUMERIC en coma, detectar_relaciones() separa los extremos de un rango con punto y coma, como con OutDec: decia [0,125, 0,75]. - Si sf esta instalado y no se puede cargar, el perfil no dice que falta. Con LC_NUMERIC en coma su dependencia units no carga; el motivo decia "falta el paquete opcional 'sf'" y el remedio "Instalar el paquete". Ahora dice que esta instalado pero no se pudo cargar en la sesion, y nombra LC_NUMERIC cuando es la causa. - Bajo el locale C los avisos del paquete llegan con su texto intacto, como los errores desde la ronda 31: warning() pasaba el mensaje por gettext() y conditionMessage() devolvia mtrica(s). - El ejemplo de ?pack_validadores (la plantilla de un pack propio) calcula el digito verificador con enteros: con scipen = -9 rechazaba un RUT valido. Cuatro decisiones de diseno: - perfilar() acepta columnas_codigo. La regla de numeracion (ronda 28-N) dejo de tratar como identificador a un codigo que se repite, porque por su forma no se distingue de una magnitud entera que lleno su rango, y un codigo de 1 a 284 como EstCod recibia 181 outliers falsos. Pasarlo como texto o factor no lo evitaba: el paquete infiere numero. Ahora se declara: una columna en columnas_codigo no recibe limites de Tukey (n_outliers en NA, sin outliers ni posible_centinela_numerico), ni la ley de Benford, ni media, mediana y desvio, con estado_resumen_cuantitativo = "omitidos_codigo"; lo que no se evaluo va a cobertura_diagnosticos con su motivo (dos razones nuevas: outliers y ley_benford por codigo_declarado). Conserva lo que describe sus valores: minimo y maximo -el tramo de codigos-, moda, distintos, conteos y faltantes disfrazados. Vale igual por perfilar_dbi() (tambien en resumen_tabla, por SQL, bloques y spool, con una fila de cobertura no_aplica; el nombre se valida aunque no haya muestra), perfilar_por() (se recorta de la rebanada que no la tiene), perfilar_coleccion() y analizar() (sus distribuciones publican solo los cuantiles 0 y 1 de la columna). comparar_perfiles() declara un cambio de la declaracion como cambio de configuracion. El sentido contrario -forzar que se midan las magnitudes de una columna que parece numeracion- no se agrega: desde 28-N solo es numeracion la columna densa y casi sin repetir, donde Tukey no tiene que acusar salvo un salto de escala, que ya rompe la guarda. - El id de las sugerencias de proponer_modelo() sale de su contenido. Era sugerencia-NNNN, numerado tras ordenar por nombre de columna: una columna nueva corria los numeros, y una regla que nombraba sugerencia-0002 -la NoNulo de edad- media la de apellido en la corrida siguiente; la deriva por regla publicaba un deterioro error de -0,44 que era del id. Ahora es la metrica y sus atributos en su orden (NoNulo:edad, ReglaIntegridadIntraEntidad:depto,codigo), con la entidad delante de cada atributo cuando la sugerencia liga dos tablas (ReglaIntegridadInterEntidad:personas.id,visitas.persona_id), y la metrica sola si no tiene atributos (EntidadDuplicada). La entidad de una sola tabla no entra -es el nombre del perfil y cambia con la expresion perfilada- ni la configuracion. %, , y # se escriben %25, %2C y %23 (y . como %2E en una entidad), y dos sugerencias del mismo contenido -una tabla que repite un nombre de columna- se distinguen con #2, #3 en el orden de las columnas, nunca por la posicion en la propuesta. El id es la metrica_instanciada de modelo_desde_propuesta(), medir(), el tablero, el historico y la deriva: medido, una sugerencia propuesta, materializada, medida, evaluada, guardada y comparada entre dos corridas con una columna agregada en la segunda conserva su id, y la deriva da delta 0. - La NoNulo con faltantes disfrazados se mide (revierte 31-R H6). La ronda 31 la dejaba con estado faltantes_disfrazados, sin medir ni materializar. Ahora se propone activa y lista y se mide contandolos: los valores que el perfil conto como disfrazados -con la misma funcion y la misma declaracion de sentinelas_numericos y cadenas_ausencia- van a valores_nulos de su configuracion, y sobre 40 valores con un NA y tres -999 mide 0,90 y no 0,975 (0,85 en la columna de texto con cinco disfrazados). Si no se pueden identificar -proponer_modelo() sin datos- se mide igual con los valores crudos, y la justificacion dice cual de las dos se hizo. - El id por contenido es seguro en cualquier nombre y en cualquier sesion. Un nombre de columna que no es UTF-8 valido (declarado bytes) se escribe en el id con %XX, byte a byte, en vez de hacer abortar medir(); y medir() ya no avisa "unable to translate" bajo el locale C con ids no ASCII (unia las mediciones con rbind() sobre una lista nombrada por instancia; ahora sin los nombres). - El paquete ya no tapa stop() ni warning() de base. La ronda 31 habia definido en el espacio de nombres un stop() -y la 32 un warning()- que armaban la condicion sin gettext(), para que bajo el locale C el texto de un error o un aviso que se guarda como dato no saliera escapado; con devtools::load_all() tapaban tambien a los de base en la sesion. Ahora son .detener() y .avisar(), con nombre propio, y las 711 llamadas del paquete los nombran; ningun nombre de base queda tapado, y una prueba recorre el codigo del paquete y no encuentra otra llamada a stop() ni a warning() de base. El comportamiento no cambia. El mensaje del motor sin proteccion (decision D8, antes de la ronda 33): - Sin proteccion, el mensaje de un error del motor se publica entero. Una vista que castea a int una columna de texto con N/D -un faltante disfrazado, que es justo el diagnostico- hace fallar la lectura, y perfilar_dbi(proteger_datos_personales = FALSE) publicaba Conversion failed when converting the nvarchar value '[literal omitido]' to data type int. | '[literal omitido]': el literal y el SQL se omitian con o sin proteccion. Sin proteccion el paquete ya publica valores de todas las columnas -modas, ejemplos, patrones-, y omitirlos ahi no protegia nada y quitaba el diagnostico (decision D8: el cometido principal del paquete es el diagnostico de la calidad de los datos). Ahora, sin proteccion, el mensaje se publica como lo dijo el motor -solo sin las vinetas ni los cortes que agrega cli- en resumen_tabla$sql$motivo, resumen_tabla$cobertura$motivo, meta, los motivos de perfilar_coleccion() (cobertura_coleccion, cobertura_metricas, meta$lecturas) y los avisos de consola: ... the nvarchar value 'N/D' to data type int. | 'SELECT COUNT(*) AS "n_total_consulta", COUNT("codigo") ...'. Con la proteccion activa no cambia nada: el piso, la lista de lo permitido y la fuga residual declarada quedan como estaban. La decision se toma en la entrada y llega a la lectura del mensaje por un solo camino: perfilar_dbi() y perfilar_coleccion() la fijan al entrar y la devuelven al salir, tambien en error, y los treinta sitios que publican un mensaje del motor la leen en .mensaje_motor(); coleccion(), plan_perfilado_dbi(), estimar_costo_coleccion() y relaciones_coleccion() no tienen la bandera: por su cuenta tapan como con proteccion, y llamadas desde una que no protege heredan la de esa. Medido en SQL Server, PostgreSQL y duckdb. Y la regla "si la llamada protege", que estaba escrita cuatro veces en la via DBI, es una sola (.protege_argumentos()), y lee la bandera por su nombre exacto, como la validacion: con $ casaba tambien un nombre mas largo. - Sin proteccion, perfilar_coleccion(conservar_perfiles = TRUE) ya no cierra los perfiles de las demas tablas cuando una no se perfila. Una tabla que no se pudo perfilar -o declarada tipo = "vista"- quedaba registrada con sus valores protegidos desconocidos, y los perfiles retenidos de las otras se cerraban igual, con proteger_datos_personales = FALSE: la moda Montevideo de una columna de ciudades salia [valor protegido] y meta$proteccion_personal$otras_tablas decia "No verificada". Es la misma propiedad que el mensaje del motor: una proteccion que el usuario apago y que costaba diagnostico. Ahora el piso entre tablas corre solo con la proteccion activa; con ella no cambia nada. Rendimiento: - La normalizacion Unicode es lineal en el largo de la celda. El orden canonico de las marcas y el retiro de acentos crecian la salida con c() dentro de un while, una vuelta por caracter: detectar_discordancias() tardaba 15,6 s con tres celdas de 100.000 caracteres y perfilar(normalizar = TRUE) 15,5 s; ahora 2,2 s y 3,1 s, con la misma salida. Decision documentada: - Los nombres de fila cuentan en max_bytes_muestra: el paquete los trata como datos y la muestra los materializa, asi que la misma tabla con nombres de fila de texto se muestrea en menos filas con el mismo tope. El recorte se declara en cobertura_diagnosticos, y la ayuda de perfilar() ahora lo dice. La documentacion: - La ayuda declara dos familias que la regla tapa de mas. El fijo que no es de nadie escrito como numero de planilla con su prefijo (59.822.877.006), que desde la ronda 31 se lee por su parte nacional: 3,2 %, como pegado, y 4,4 % con los miles y los decimales en cero o con espacios -sus primeras ocho cifras tienen la forma de una cedula entera, como en el celular-; la ayuda decia que las mismas cifras se tapaban en todas las escrituras. Y el entero de ocho cifras que empieza con 9 (90 a 99,9 millones), 3,3 % en cualquier escritura: es un celular protegido sin su cero; la ayuda daba "uno o dos de cada cien", medido entre 10 y 70 millones. Y lo que suma la lectura nueva del numero de planilla: con el punto medio, 5,3 % (7,8 % con ,00; 1,3 y 4,0 % antes), y con el agrupado indio, que ya se tapaba uno de cada cinco por sus grupos de dos, 20,3 % (32,0 % con .00; 17,2 y 29,3 % antes). Las familias nuevas estan en data-raw/medir_tapado_de_mas.R. Ronda 31: la misma respuesta en cualquier sesion, la proteccion otra vez y una regla por hecho Tres refutaciones. La de la sesion encontro ocho defectos -bajo un locale turco ABRIL 2024 no era una fecha ni point(1 2) WKT; con scipen o OutDec el nombre de una regla cambiaba y la deriva perdia el par, la proteccion tapaba los estadisticos de una columna no protegida y perfilar_dbi() publicaba divergencias entre dos cifras iguales; en Adak dos instantes salian con la misma etiqueta-; la de la proteccion, seis -la coleccion descartaba valores que el mensaje del motor lleva, y el celular escrito con separador de miles salia en claro-; la de dos funciones que contestan el mismo hecho, diez -un factor con nivel NA era clave para detectar_claves() y clave con ausentes para perfilar(), y aplicar() no eliminaba las columnas que el plan llamaba duplicadas-, y la auditoria uno mas: una conversion regional que no se ejecuta salia lista por un camino que la ronda 30 no cerro. La proteccion: - La coleccion guarda los valores que el mensaje del motor lleva, con la misma regla que lo publica. .valores_que_lleva_el_motor() decidia con su propia lectura que valores protegidos guardar para el mensaje del motor de las demas tablas -"un numero de seis cifras con hasta tres signos entre las cifras" y "a lo sumo una palabra fuera de lo permitido"- y la segunda pasada publicaba con otra: con SQL Server en sueco, que cita la celda sin comillas, perfilar_coleccion(conservar_perfiles = FALSE) publicaba en sus tres campos de motivos y en el aviso de consola el celular protegido 099;123;456 (y las demas escrituras con un signo que no une: |, tres espacios, " / "...) y el nombre "Florencia Martinez" citado por una vista con la columna Florencia.Martinez. Ahora la seleccion le pregunta a la segunda pasada si deja pasar algo del valor con toda palabra permitida -cualquier palabra puede ser el nombre de una columna de otra tabla-: se guarda todo valor con letras y el numero que el mensaje no omite entero. Y el piso con esos valores pasa ademas antes de las dos pasadas, con el valor entero a la vista: con "Ana Florencia Martinez" la segunda omitia "Ana" y salia "Florencia Martinez", y con 099 ' 123 ' 456 la primera omitia lo citado y salian 099 y 456. Para no correr la segunda pasada valor por valor (dos milisegundos cada uno), se le pregunta por la forma del valor -cifras y tramos de letras resumidos-, que solo puede guardar de mas. - El aviso de consola de una coleccion pasa por el mismo piso final que sus tres campos de motivos. Con la vista antes que su tabla base en coleccion(), el objeto quedaba limpio y el aviso "El resumen SQL se calculo y se devuelve, pero la muestra no" de la vista publicaba Campos Cadena, la celda de la base, porque se emitia antes de leerla. Los avisos de cada tabla se juntan y se emiten al final, con su clase, despues del piso, con o sin perfiles retenidos. - El celular escrito como numero de planilla con separador de miles, o en cientifica con un espacio antes del exponente, es la misma cifra. "59.893.425.706,00", "59,898,617,353", "59 898 617 353", "59'898'617'353" -con o sin ,00, con + delante- y "5.9895845098 e+10" se publicaban frente al celular protegido nacional "098 617 353": 60 de 60 notas en claro en la copia de analizar() y los tres ejemplos del perfil, mientras sin miles, con el exponente pegado o frente al protegido E.164 se tapaban. Los grupos de tres partian el 598 y la parte nacional se buscaba en los grupos como venian. La lectura del numero de planilla -la de las agujas, el piso numerico y el texto- da ahora la misma cifra entera para todas esas escrituras, y la parte nacional sale de la cifra. Con data-raw/medir_tapado_de_mas.R, ninguna familia de antes cambia -tampoco los importes con miles- y las nuevas, con las mismas cifras en cada escritura, dan: el importe de ocho cifras con miles y ,00 1,8 %, como antes, y en cientifica con un espacio antes del exponente 1,8 %, como sin el espacio (antes 0 %); el celular que no es de nadie escrito con miles, 3,0 %, como pegado con su prefijo (antes 0 %), y con miles y ,00 o con espacios 4,4 % (antes 1,5 %: sus primeras ocho cifras tienen la forma de una cedula entera). La ayuda lo declara. - La copia de analizar(conservar_datos = TRUE) lee una funcion, una llamada y una formula como las imprime. Se leian por su codigo sin atributos ni fuente, y se devolvian antes de recorrer sus atributos: structure(function() NULL, nota = ), la llamada y la formula con ese atributo, el comentario # titular de una funcion con srcref -lo que deja una sesion interactiva- o de una expresion, y un entorno que guarda una funcion asi quedaban en la copia. Se leen ahora como print() las escribe -con sus atributos y su fuente- y por sus atributos; y el recorrido de atributos, que estaba escrito tres veces, es uno y pasa por el piso tambien el NOMBRE de cada atributo: attr(x, "2.037.187-5") <- TRUE quedaba en la copia. - analizar(conservar_datos = TRUE) ya no aborta con una columna lista de puros NULL. Con la proteccion puesta, "primer argumento debe ser un vector": el piso de la copia juntaba los textos de la lista y unlist() de puros NULL es NULL. - La ayuda declara dos familias que el reproductor tapa de mas y no tenian cifra. La version de cinco partes ("v2.76.527.6327.3"), quince de cada cien, y el celular que no es de nadie, entre dos y tres de cada cien (2,0 a 2,5 % con 400 por escritura; 3,0 % con 4.000), como ya daba el reproductor desde la ronda 27: la ayuda daba la de cuatro partes y el fijo. Dos funciones, dos respuestas: - Las cifras de magnitud se calculan solo con valores finitos tambien en la via SQL. Con bloque_filas, la mediana contaba los infinitos que minimo, maximo y media de la misma fila excluian: c(-Inf x3, 10, 20, 30, 40) daba mediana 10 contra 25. Sin bloques, en SQLite, duckdb y PostgreSQL, una columna de punto flotante con un Inf dejaba en NA minimo, maximo, media, mediana y desvio "porque la columna no es de la magnitud que estas metricas suponen", y la corroboracion publicaba como causa medida que la columna guardaba texto. Un infinito es un numero: cuando el motor lo responde, las magnitudes se vuelven a pedir sobre una subconsulta que lo deja afuera, con la misma maquinaria de siempre, y su sql lo muestra. Lo mismo por la sonda de magnitud, que ahora pide tambien MIN -un -Inf queda al principio del orden y la mediana salia 10-, y en perfilar_coleccion(). Donde la consulta no sabe escribir el infinito, o sobre una muestra del motor, quedan no_disponible con ese motivo y no con el de "no es numero". La decision esta en ?perfilar_dbi. - La causa "el motor guarda texto" se mide. En SQLite se cuentan con typeof() los valores guardados como texto o blob, el conteo va al motivo, y la corroboracion solo publica la causa medida cuando lo hay. - Un factor con nivel NA no es clave para detectar_claves(), sugerir_clave() ni referencial(). El perfil lo cuenta como faltante desde la ronda 30, y estas tres leian la ausencia sobre el factor crudo: un codigo unico salvo una fila en el nivel NA salia clave exacta con 40 distintos y "identifica cada fila sin repetir", la columna que perfilar(clave = ) refuta por ausentes. Pasan por la misma entrada que el perfil. - aplicar() elimina las columnas que el perfil llama duplicadas. Dos columnas que difieren solo por el nivel NA, o un POSIXlt y su POSIXct, salian duplicadas en el perfil y en el plan, y aplicar() las registraba fallida, 0 cambios, "dejaron de tener contenido duplicado", sobre la tabla original. La comparacion es una sola y normaliza sus dos columnas como la entrada del perfil. - analizar()$distribuciones recibe las declaraciones de argumentos_perfil. Con sentinelas_numericos = c(-999, 999) el perfil publicaba maximo 60 y los cuantiles 999; con un universo declarado, 20000 contra 999999 y 3 marcas distintas contra 4. Lo que queda fuera del universo no entra en las frecuencias ni en los cuantiles, y un centinela declarado sale de los cuantiles como sale de minimo y maximo; en las frecuencias sigue contando, como en n_distintos y moda. distribucion_valores() sola no cambia. - (Revertido en la ronda 32: la NoNulo con faltantes disfrazados se mide contandolos.) analizar() no mide la NoNulo que la propuesta deja inactiva por faltantes disfrazados. La fila de decision_medicion decia medida = TRUE y "queda inactiva hasta normalizarlos", y el tablero publicaba 0,975 donde, con los disfrazados que el perfil cuenta, la completitud es 0,90 y 0,85. Sale con estado faltantes_disfrazados, como nombre_repetido: no se mide ni se materializa hasta normalizarlos o reconocerlos en valores_nulos. - distribucion_valores() cuenta un POSIXlt como su POSIXct: lo declaraba tipo_no_comparable, con 0 analizados, donde perfilar(), inferir_tipo() y medir() leian 7 presentes y 4 distintos. Pasa por la entrada del perfil. - desambiguar_fecha_en_origen cuenta en la unidad de su hallazgo: publicaba n_afectadas = 40 -las filas- con la unidad "formato" de un hallazgo que declara 2 formatos. Es la regla que las acciones informativas ya seguian, escrita ahora una sola vez. - Sin moda, la moda es NA tambien con la proteccion activa. Una columna personal sin valores repetidos publicaba [valor protegido], la misma escritura que una que si tiene moda, en perfilar() y perfilar_dbi(). La proteccion tapa un valor; no inventa uno. frecuencia_moda = 1 ya decia que no se repetia ninguno. - Una conversion regional que no se ejecuta con ninguna configuracion sale bloqueada. La ronda 30 lo resolvio para la columna que no es texto; sobre c("1.234,56", "100.000", "1e5", ...) -1e5 no responde al formato regional- seguia lista con un motivo que decia que no era ejecutable. El plan le pregunta al conversor con las configuraciones de punto_sin_coma y coma_sin_punto; la que solo espera una de ellas sigue lista. La misma respuesta en cualquier sesion: - Bajo un locale turco ABRIL 2024 es una fecha y point(1 2) es WKT. ignore.case = TRUE y (?i) sobre texto ASCII pliegan con las tablas de caja del locale, y bajo tr_TR la I y la i no se pliegan: detectar_formatos_fecha() daba %B %Y en 0,25 contra 1 y inferir_tipo() texto, y una columna WKT en minusculas no se reconocia. Ningun patron del paquete usa ya ignore.case ni (?i): el vocabulario se escribe con las dos cajas de cada letra (.regex_sin_caja()), en los 36 sitios -fechas, WKT, motores, permisos, correos ofuscados, ISBN-, y una prueba recorre el espacio de nombres para que no vuelva. El WKT le llega a sf en mayusculas ASCII: el lector de GDAL tambien comparaba con la caja del locale y perdia tres de cuatro geometrias. - El nombre de una regla de perfiles_madurez() no depende de OutDec ni de scipen. Resultado > 0,9 en una sesion y Resultado > 0.9 en otra no se apareaban: la deriva por regla perdia la fila y comparar_evaluaciones() afirmaba "Cambio el perfil de evaluacion" sobre el mismo perfil. El umbral se escribe con punto y la escritura mas corta que vuelve al valor, y la configuracion del perfil -que guarda la condicion con deparse(), que escribe los numeros con scipen- tambien: todo deparse() del paquete fija scipen = 0. Una evaluacion guardada antes sigue apareando. propiedades_regla() escribe los umbrales de la misma forma, con todas sus cifras. - scipen no mueve la proteccion. La cosecha de valores protegidos, la comparacion de las hojas numericas y las decisiones que leen un numero como texto -columna de fechas, rellenos- usaban as.character(): con scipen = -9 el 11 de una columna protegida salia 1.1e+01, pasaba el piso de seis caracteres y tapaba minimo, maximo, media, mediana, desvio y la secuencia entera de id_copia en perfilar(datos_administrativos). - La corroboracion de perfilar_dbi() compara el valor y no su escritura. Un conteo llega entero de un lado y doble del otro, y con scipen = -9 "12" contra "1.2e+01" daba nueve divergencias falsas de nueve; con scipen = 0, 100000 entero contra 1e5 doble tambien. El mismo arreglo en el apareo de centinelas sobre una columna integer64. - Bajo el locale C, el texto de un error del paquete entra intacto al plan, al registro de aplicar(), a la guia, a analizar() y al HTML. stop() pasaba el mensaje por gettext(), que sin catalogo lo devuelve en la codificacion nativa, y bajo C escapaba numrico. Los errores del paquete se arman ahora como una condicion con el texto tal cual. - Un instante se escribe en UTC y con su rotulo en todas las salidas. distribucion_valores(), las etiquetas de perfilar_por() y la clave de meta$normalizacion_fusiones escribian una fecha-hora sin zona en la hora de la sesion: en America/Adak dos instantes distintos salian con la misma etiqueta y la normalizacion los fundia (n_distintos 3 contra 4), y la moda de una columna en Asia/Tokyo salia en UTC en el perfil y en hora de Tokio, sin rotulo, en la distribucion. La zona declarada de la columna sigue en zona_horaria_origen. - Las cifras publicadas como texto no salen en notacion cientifica. Con scipen = -9, comparar_perfiles() publicaba 1.2e+01, analizar_tiempo() "1e+00 dias", detectar_duplicados_aproximados() "distancia 0e+00 con jw (umbral 1e-01)", la evidencia de faltantes_disfrazados el centinela -99 como -9.9e+01 -y esa evidencia se relee como lista de marcadores- y el reporte "8.2e+00 Kb", "12 de 1.2e+01" y "Se muestran 1e+02 de 101 filas": todo numero que el HTML escribe pasa por la escritura publicada. Con coma decimal, detectar_relaciones() separa los extremos de un rango con punto y coma, como comparar_perfiles(): decia [0,10, 0,75]. - perfilar_dbi() no deja escapar el aviso crudo de bit64. Un BIGINT mayor que 2^53 en SQLite emitia "integer precision lost while converting to double" con warn = 0; el aviso se recoge donde se convierte y la perdida se declara como el resto. Ronda 30: la proteccion otra vez, el paquete bajo el locale C y las clases de columna Tres refutaciones. La de la proteccion encontro cinco defectos -el mensaje del motor publicaba un numero con separadores que la lista de lo permitido tomaba por signos, y la lectura de los decimales en cero de la ronda 29 tapaba uno de cada cinco importes de siete cifras con centavos en cero-; la del paquete entero bajo el locale C, ocho -tolower() de base no bajaba las mayusculas con tilde, una columna si/no dejaba de ser logica, y el orden de la intercalacion cambiaba los identificadores de las sugerencias-; la de las clases de columna por todo el catalogo, nueve -una columna vctrs_rcrd hacia abortar 22 de 29 llamadas-. La proteccion: - El mensaje de un error del motor lee el numero como lo lee el piso. La segunda pasada tenia su propia lectura de "un numero escrito con separadores" -cifras unidas por uno solo de ., ,, :, /, espacio o guion- y el piso otra: de 67 separadores entre los grupos de una cedula o un celular, el piso reconocia el valor en 45 y el mensaje de SQL Server con la sesion en sueco, neerlandes, finlandes o checo -que cita la celda sin comillas- lo publicaba en 39 de esos 45: 099 - 123 - 456, con espacio duro o fino, punto medio, raya, dos espacios, tabulador o salto de linea. perfilar_dbi() lo publicaba en sql$motivo, cobertura$motivo, meta$proteccion_personal$fuera_de_muestra y en el aviso de consola, y perfilar_coleccion() en sus tres campos de motivos. Ahora es una sola regla: el mensaje lee los numeros con la de digitos del piso. - El mensaje del motor pasa tambien por el piso con los valores que la llamada conoce. Un valor escrito solo con lo permitido -los apellidos Campos Cadena, dos palabras del vocabulario- es la celda citada, y salia entero: en perfilar_coleccion() con conservar_perfiles = FALSE, el mensaje de una vista citaba la celda de la tabla base que la coleccion habia leido. Cada perfilar_dbi() de una coleccion suma los valores protegidos que lee -solo los que un mensaje puede llevar: los que se escriben con lo permitido-, el mensaje pasa por el piso con ellos y los tres campos de motivos tambien, al final. Y un nombre de columna igual a uno de esos valores deja de exceptuarse en el mensaje, con la comparacion del piso: la docente "Florencia" de una planilla pivoteada por docente salia como 'Florencia'. Queda declarado lo que la llamada no leyo: la celda que hizo fallar la lectura de la muestra, escrita solo con lo permitido o igual al nombre de una columna, se publica. - El celular escrito como numero de planilla con cuatro ceros o mas, o con exponente de tres cifras, es la misma cifra. "59894215856.0000" -un DECIMAL(15,4), el MONEY de SQL Server- y "5.9894215856e+010" -el printf de Windows- no se leian en un texto: 80 de 80 notas en claro en la copia de analizar() y tres en los ejemplos del perfil, frente al celular protegido. El texto leia uno o dos ceros y el exponente de dos cifras. - El importe redondo ya no se tapa como un documento sin su verificador. Regresion de la ronda 29: el decimal de ceros se leia como el entero y alimentaba la regla del documento parcial, y un importe de siete cifras con centavos en cero -"5460537.00", "5460537,00", dentro de una frase- se tapaba en uno de cada cinco casos con el universo de data-raw/medir_tapado_de_mas.R (en la ronda 28, ninguno); en la copia de analizar(), 80 de 20.000 importes. La cifra entera de lo escrito como numero de planilla es ahora solo el documento ENTERO -el celular de once cifras, la cedula con su verificador-: 0 %. El reproductor mide los importes por escritura sobre las mismas cifras enteras, y la ayuda declara lo que ya pasaba en la ronda 28 sin decirse: un importe de ocho cifras con centavos se tapa 1,8 % de las veces, como el entero de ocho cifras, y uno de siete con un solo cero, 2,1 %, porque se lee tambien como la cedula con su verificador tras un punto. - La copia de analizar(conservar_datos = TRUE) pasa por el piso todo atributo de todo objeto que conserva. Quedaban en claro el comment y la label de una columna POSIXlt, el units de una columna protegida -la cedula guardada como numero, la fecha de nacimiento-, los nombres de columna de un data.frame anidado en una columna lista, un entorno con atributo name -environmentName() lo leia como el de un paquete- y una llamada o una funcion en una columna lista. Las llamadas, las formulas y las funciones se leen por el texto que las escribe, y la funcion tambien por su entorno: si llevan un valor protegido, la copia lleva "[valor protegido]" en su lugar, como un entorno. El locale C: - La caja se pliega con el mapa del paquete en todo el texto del usuario. La inferencia de logicos, el detector de faltantes disfrazados, el plan y aplicar() (la conversion a logico y la de marcadores a NA) bajaban la caja con tolower() de base, que bajo C no baja una mayuscula acentuada: una columna SI/NO con tilde era logico en una sesion UTF-8 y texto en un Rscript de cron -otro hallazgo, otro plan, otros datos limpios-; un plan armado en RStudio quedaba fallido al aplicarlo bajo C; y SIN INFORMACION con tilde no se contaba como faltante disfrazado (8 de 12 contra 12 de 12, con sospechoso en vez de error, y aplicar() dejaba 4 celdas sin pasar a NA). Todos los sitios pasan por un pliegue para comparar (.minusculas_para_comparar(): declara, recorta y baja con el mapa de normalizacion()); el vocabulario del paquete y de los motores -palabras de SQL, tipos, PRIMARY, hexadecimales- se baja con chartr() ASCII, que no consulta LC_CTYPE ni bajo un locale turco. Ya no queda en el paquete ninguna llamada a tolower(), toupper() ni casefold(), y una prueba lo comprueba recorriendo el espacio de nombres. - aplicar() escribia ab en una columna latin1 bajo C. El pliegue de caja (convertir_minusculas, convertir_mayusculas, convertir_titulo) pasaba el texto latin1 a chartr(), que lo traducia al nativo y escapaba lo que no podia: la I con tilde salia como el texto , en los datos, y el registro contaba 7 celdas cambiadas contra 4. Y la reparacion del texto que no es UTF-8 valido leia la celda en la codificacion de la sesion: bajo C una celda con un byte suelto perdia tambien sus letras validas. Lo latin1 se convierte y lo invalido se lee como UTF-8, sin mirar el locale. - requisitos_motor("PostgreSQL") no reconocia el motor. El nombre se limpiaba de lo que no es letra minuscula ANTES de bajarlo, y el nombre como lo publica el propio catalogo se perdia; ahora se baja primero. - Un importe con el euro sin marca no se convertia bajo C. La descomposicion del numero con moneda (.componentes_numero_texto()) no declaraba el texto antes de \p{Sc}: sobre un CSV leido sin encoding, el perfil proponia convertir_numero_regional en los dos locales, pero bajo C la comprobacion del plan la daba por no ejecutable ("no responden al formato numerico regional") y aplicar() dejaba la columna character; en una sesion UTF-8, numeric. Ahora declara antes de la primera primitiva, y lo que no es UTF-8 valido no es un numero en ninguna sesion (bajo un locale UTF-8 abortaba el trimws(); bajo C se leia por bytes). - El orden de las tablas publicadas ya no sigue la intercalacion del locale, y con el, el id de cada metrica propuesta. proponer_modelo() ordenaba sus filas por el nombre de la columna con order() de base ANTES de numerar sugerencia-NNNN: sobre la misma tabla, sugerencia-0003 era el Formato de Fecha de Alta bajo C y el de depto bajo es_UY.UTF-8 (7 de 22 metricas de analizar() median otro atributo), y una regla que nombraba la metrica por su id daba 1 en un Rscript de cron y 0 en RStudio. El desempate de formatos_fecha ponia %Y-%m-%d o %d/%m/%Y primero segun el locale, y eso llegaba a la evidencia, a plan$parametros$formatos, a inferir_tipo() y al reporte; las asociaciones, las dependencias y la medicion salian en otro orden. Todo order() y sort() del paquete pasa ahora por .orden_por_bytes() / .sort_por_bytes(): el texto por sus bytes (.clave_bytes()), con method = "radix", como ya hacian detectar_deriva_calidad() y los patrones. Una prueba recorre el espacio de nombres y no encuentra otro order(), sort() ni rank(). - Un nombre de columna con un byte latin1 suelto tumbaba analizar() en una sesion UTF-8. Es lo que deja read.csv(check.names = FALSE) o fread() sobre un CSV Windows-1252: el escape del nombre para la busqueda de valores protegidos en el vocabulario (gsub(fixed = TRUE), de la ronda 29) quedaba fuera de su tryCatch y abortaba con "input string 1 is invalid in this locale"; bajo C terminaba. Y perfilar() avisaba cuatro veces al partir los nombres de los hallazgos compuestos, y con options(warn = 2) abortaba. Lo que no es UTF-8 valido se corta por bytes, devolviendo a las piezas la marca de origen (la regla de la ronda 27), y no se lee por caracteres en ninguna sesion. La misma regla escrita en 55 sitios -todo grepl() con fixed = TRUE y un patron ASCII, entre ellos los siete "[valor suprimido]" del historico, la evaluacion y el tablero- busca ahora por bytes, que es exacto y no avisa ni aborta en ningun locale. - Los nombres de columna latin1 salian como texto descripcin bajo C. paste() traduce al nativo lo marcado latin1, y bajo C lo nativo es ASCII: detectar_claves()$columnas publicaba fecha + descripcin, detectar_duplicados_aproximados() ponia descripcin en cada hallazgo -un nombre que no indexa la tabla, contra lo que promete ?perfilar-, proponer_modelo() y analizar() lo mismo en atributos, origen y decision_medicion$objeto, y una columna llamada como un valor protegido salia [valor protegido]. Un nombre solo se publica ahora tal como llego, y una etiqueta que junta varios se arma por bytes, con lo latin1 convertido (.pegar_texto()): la misma en los dos locales. - clasificar_variables() declaraba ausente un nivel que observo, bajo C. Comparaba los niveles crudos con los valores ya declarados UTF-8, y %in% entre un texto sin marca y uno UTF-8 no casa bajo C: 4 niveles observados de 4 y uno ausente. Ahora compara con la clave estable de los identificadores. - La evidencia citaba los ejemplos con encodeString(). Bajo C escapa como punto de codigo todo lo que no es ASCII: ejemplos: "Jos\u00e9 " (33 bytes) contra la e con tilde en una sesion UTF-8 (29), en hallazgos(), el plan, aplicar(), guiar_limpieza(), analizar() y el HTML de reportar(), y el ejemplo dejaba de casar con el de la tabla de patrones. Las 16 citas del paquete -tambien las de la reparacion de codificacion, las dependencias, el historico y las justificaciones del plan- pasan por un citador propio que escribe lo que encodeString() escribe en una sesion UTF-8, en cualquier locale; una prueba comprueba que no queda otro encodeString(). - detectar_claves() abortaba con options(warn = 2) bajo C sobre una tabla con un nombre UTF-8 como a\u00f1o: la busqueda de filas repetidas armaba un data.frame con los nombres, y bajo C el nombre no se traduce al nativo. Ahora busca sin los nombres. - El cierre por comparacion. El catalogo de funciones exportadas (perfil, hallazgos, plan, aplicar(), guia, patrones, tipos, formatos, claves, dependencias, asociaciones, duplicados, distribucion, clasificacion, analizar(), propuesta, modelo, medir(), perfilar_por(), comparar_perfiles() y el HTML de reportar()) sobre tres tablas en seis marcas y siete tablas minimas, en dos procesos -LC_ALL=C y es_UY.UTF-8- y con options(warn = 2), encontro lo que quedaba de las mismas familias: los objeto_medible de medir() (df$descripcin[1]) y la evidencia columna=valor de duplicados se pegaban con paste0(); el resumen de hallazgos de comparar_perfiles(), las evaluaciones, el tablero, la agregacion y el plan de imputaciones agrupaban con split()/factor()/table(), que ordenan los niveles de un texto con la intercalacion del locale; las celdas del reporte se unian con paste(); la conversion de fechas de aplicar() y la serie de analizar_tiempo() recortaban el texto antes de declararlo; y la decision de si un texto sin marca es legible miraba l10n_info(), asi que bajo C aplicar() transformaba por bytes la celda que el perfil declaro codificacion_invalida y el reporte escribia Telfono donde una sesion UTF-8 escribe Tel\xe9fono. Y uno que no era del locale: sobre una columna sin ningun valor decodificable, la propuesta armaba ValoresPosiblesPorExtension con un dominio vacio y analizar() avisaba que su metodo fallo; ahora no la propone. Con eso el catalogo da las mismas salidas, byte a byte y marca a marca, en los dos locales. - print() de clasificar_variables() abortaba segun el locale. Las listas de niveles se publicaban como llegaron: bajo C un nivel UTF-8 sin marca, y en una sesion UTF-8 un nivel con un byte latin1 suelto, hacian abortar format.AsIs() ("invalid multibyte string"). Ahora lo legible se publica declarado -los mismos bytes- y lo que no es UTF-8 valido como lo muestra la consola, igual que lo declarado bytes. Las clases de columna: - Una columna cuyos valores no se pueden escribir como texto es compuesta: una vctrs_rcrd -o un vctrs_vctr sin conversion declarada- hacia abortar 22 de las 29 llamadas del catalogo ("Can't convert x to ") desde tres sitios. Se pregunta por la propiedad, una vez por columna, y entra por la puerta de la matriz y la tabla anidada: tipo_compuesto_no_analizado, con su clase en el hallazgo. Las otras columnas publican las mismas cifras que sin ella. - Un integer(0) o un NULL en una lista no es una ausencia estructural: la ausencia estructural tenia su propia regla para listas y "explicaba 10 de las 10 ausencias" de una columna con n_faltantes = 0; seguir su sugerencia fabricaba un valor_fuera_de_aplicabilidad sospechoso. Ahora cuenta la ausencia del perfil, is.na(). - analizar() mide una columna de nombre vacio o NA con ese nombre, el que publica el perfil: abortaba con "atributos debe contener nombres de columna no vacios". instanciar() acepta el nombre que tiene la tabla; uno que no tiene sale no_medible en medir(). - Con nombres de columna repetidos la traza apunta a la fila de su columna: la del outlier de la segunda x apuntaba a la fila del de la primera, como disponible. La columna viaja por posicion desde el detector hasta la traza; sin posicion y con un nombre repetido, la traza queda no_disponible. Lo mismo el par de relacion_orden_columnas y el conteo de subnormales. - Una columna sin un valor comparable por fila se declara en detectar_relaciones() y detectar_discordancias(): una units hacia abortar las relaciones aunque no fuera candidata, y una lista, una sfc o una units hacian abortar las senales con errores de R. Una matriz se comparaba por sus elementos (1:1 con cobertura 0,5 sobre tres filas). Sus pares salen sin_comparar con motivo_poda = "columna_no_comparable", y la senal sale con n_evaluadas = 0 y el motivo. - El nivel NA de un factor es un faltante: el perfil decia 0 faltantes (y entonces no se proponia NoNulo), n_distintos salia NA, y la dependencia lo contaba entre los pares presentes, mientras distribucion_valores(), la medicion e inferir_tipo() lo leian ausente. Ahora todos lo leen ausente; aplicar() marca y elimina esas filas como el plan dice. Esto cambia una decision de la ronda 26: el nivel NA y el codigo ausente son la misma fila para los duplicados. - Un vector de bytes (raw) no pasa por las etapas que interpretan la escritura: su escritura hexadecimal se leia como texto escrito, y "0a" salia numero_como_texto con la unidad "a", y patron_raro sobre 9a. Se sigue comparando y publicando en hexadecimal. - Una conversion regional que el plan probo y no se puede ejecutar sale bloqueada: salia lista con un motivo que decia que no era ejecutable. - Un POSIXlt se perfila por su instante: su moda salia NA sin declararlo, y en una zona distinta de UTC no publicaba la fecha civil distinta ni su hallazgo. La misma columna como POSIXct y como POSIXlt da la misma fila. - En SQLite, la divergencia de una columna de tipo mixto lleva su causa: texto en una columna de afinidad numerica, que el resumen SQL ve y RSQLite convierte a 0 en la muestra. Se declara con la causa medida y no se relee la columna como texto; la decision esta en ?perfilar_dbi. - Con dos columnas del mismo nombre analizar() no aborta: dos NoNulo sobre x eran "el mismo instrumento" y el analisis entero se caia. Una sugerencia sobre un nombre que la tabla repite sale inactiva, con estado nombre_repetido, hasta que se renombren las columnas. - Una columna llamada NA no hace abortar sugerir_clave(), elegir_clave() ni perfilar_por(): su nombre no se parece a una clave (puntaje 0) y se selecciona por su posicion. Ronda 29: la proteccion otra vez, el texto y la codificacion, y la misma tabla por todos los caminos Tres refutaciones. La de la proteccion encontro ocho defectos -el mapa de la imputacion de una columna protegida salia en claro, y el mensaje del motor citaba el valor sin comillas, en otros idiomas o con comillas que la limpieza no conocia: la limpieza paso de tapar formas a publicar solo lo permitido-; la del texto y la codificacion, ocho -la reparacion de codificacion dependia del locale y la tabla de clases combinantes tenia 25 de 51 valores mal-; la de la misma tabla por todos los caminos, seis -con bloque_filas, una sola columna integer64 truncaba a entero las medias de toda la tabla-, y el costo de instrumentar, que llevaba 23 filas a un minuto. La proteccion: - El mensaje de un error del motor publica solo lo permitido. La ronda 28 tapaba los literales ENTRE COMILLAS, y cada forma nueva de citar se escapaba: duckdb cita el valor sin comillas -Type INT64 with value 59899123456 can't be cast, Could not cast value 59899123456 to DECIMAL(9,0), (59899123456 * 1000000000), y strptime repite la cadena en otra linea-; SQL Server, con la sesion en sueco, neerlandes, finlandes, checo o noruego, tambien, y en polaco o hungaro la cita entre una comilla baja que la lista no tenia -14 mensajes de 34 idiomas-; y un valor con una comilla adentro dejaba visible su cola -"Ana "Anita" Suarez Gomez" salia con Suarez Gomez"-. perfilar_dbi() los publicaba en sql$motivo, cobertura$motivo y meta$proteccion_personal$fuera_de_muestra, y perfilar_coleccion() en cobertura_coleccion, cobertura_metricas y meta$lecturas. La lista se invirtio: despues de tapar los literales entre comillas -ahora tambien las bajas, las angulares y las de esquina, leidas de una vez de izquierda a derecha-, el mensaje publica solo el vocabulario de los mensajes de los motores, en ingles y en castellano, las palabras de SQL, los nombres de tipo, los nombres de la tabla y de sus columnas, los codigos SQLSTATE, los signos y los numeros de menos de seis cifras -las del piso de la proteccion; un numero se cuenta con sus separadores: 4.685.467-9 es uno de ocho-. Todo lo demas sale [literal omitido], y la linea que el motor repite sobre un ^ se tapa entera. Un mensaje en otro idioma conserva su SQL, sus tipos y sus nombres y se lee peor, que es el lado cerrado. - El piso entre tablas de perfilar_coleccion() cubre tambien cobertura_coleccion, cobertura_metricas y meta$lecturas, que juntan los motivos de todas las tablas: barria solo perfiles. Con conservar_perfiles = TRUE, que es cuando se leen los valores. - El mapa de una imputacion se tapa aunque sus valores sean nombres de columna. Con la docente protegida y una columna de notas por docente, el plan publicaba 1A -> Juan Perez Gonzalez en parametros[[i]]$mapa, en los dos sentidos y en la llamada por omision de analizar(): determinante y dependiente son campos de nombres, y el mapa se salteaba por ser "todo nombres". Adentro del mapa esos campos llevan valores. aplicar() ejecuta lo mismo: el mapa queda mapa_enmascarado y se resuelve sobre los datos. - Un mapa enmascarado sobre otra entrega no escribe a medias en silencio. Con la entrega que trae solo las filas a completar, aplicar() salia ejecutada con 12 celdas de 24 -las de la clave cuya fila del plan estaba tapada quedaban NA, sin motivo-, y sin el par ni el titular, ejecutada con 11 de 22 donde ?planificar_limpieza prometia fallida. Si los datos piden imputar una clave cuya fila tapada no pueden resolver, la accion queda fallida, con el motivo. - La columna duplicada que se llama como una persona protegida conserva sus acciones. columnas_duplicadas$columna_1 salia [valor protegido] y marcar_columnas_duplicadas, recomendada, quedaba bloqueada sin parametros. Los campos que refieren columnas se revisaron recorriendo las salidas de quince funciones: faltaban columna_1, columna_2, eliminar y datos_personales_sin_proteger. - El celular protegido escrito como numero de planilla protege su forma nacional. "59893897037.0" o "59893897037,0" -lo que deja pandas o una planilla- no tapaba "llamar al 093 897 037": 80 de 80 notas en claro en la copia de analizar(). Un decimal de ceros y la notacion cientifica son la misma cifra, en las agujas, en el texto y en el piso numerico. - Una columna POSIXlt ya no hace abortar analizar(conservar_datos = TRUE). El piso la recorria como lista y no terminaba nunca -"stack desbordado"-, con la proteccion puesta y sin ella no. Se tapa como la POSIXct, y cualquier lista con clase se recorre sin la clase. - El piso de la copia de analizar() recorre los BLOB, los entornos, levels y units, y la unidad del perfil no ampara un valor protegido: un units igual a un titular se publicaba en columnas$unidad. - El valor tapado ya no se come el espacio que lo sigue -[valor protegido]es exacta-: la aguja salia de format() rellena con espacios. El texto y la codificacion: - reparar_codificacion repara bajo LC_CTYPE=C lo mismo que en una sesion UTF-8. Un CSV leido sin encoding deja bytes UTF-8 validos sin marca; el perfil los media, el plan proponia la accion como lista con n_afectadas = 2 y aplicar() la dejaba fallida sin cambiar nada bajo C, porque las acciones de texto solo declaraban UTF-8 lo marcado bytes. La declaracion de que los bytes validos son UTF-8 estaba escrita tres veces con dos criterios; ahora es una sola, la de la normalizacion, y la comparten el perfil, la normalizacion y las acciones de aplicar(). - n_codificacion_normalizada cuenta recodificaciones, no marcas. Contaba las celdas cuya marca cambiaba, y la marca de un texto sin declarar la pone el locale: recortar_espacios sobre la misma columna publicaba 1 en una sesion UTF-8 y 0 bajo C con bytes de salida identicos, y decodificar José -ASCII- contaba 1. Ahora cuenta solo las celdas no ASCII cuya codificacion declarada cambio -latin1 que pasa a UTF-8-, igual en todo locale. - El orden canonico de las marcas es el de Unicode. La tabla de clases combinantes estaba escrita a mano y 25 de sus 51 valores estaban mal: con normalizacion(acentos = FALSE), el NFC y el NFD del mismo texto no se igualaban en 1770 de 17850 cadenas de base y dos marcas, y dos marcas de la misma clase en distinto orden -textos canonicamente distintos- si. Las dos tablas de la descomposicion se generan ahora desde Unicode con data-raw/normalizacion_unicode.R, y una prueba las compara con stringi cuando esta instalado. Los signos Kelvin y Angstrom se igualan a la K y a la A con anillo, como declara Unicode. - Una comilla junto a una tabulacion o un salto de linea se quita como junto a un espacio. El paso espacios llevaba al espacio comun los espacios Unicode pero no la tabulacion, el salto de linea ni el retorno, y la regla de comillas pide un espacio o el borde: "Juan" seguido de una tabulacion no se igualaba con "Juan" seguido de un espacio, aunque el paso promete colapsar los blancos. - Los dos caminos de la normalizacion dan la misma clave para las comillas, como afirmaba la ronda 17 y no era cierto: la regla estaba escrita dos veces, la vectorial casaba $ antes de un salto de linea final y su pasada de aperturas le borraba la letra de al lado a la de cierres. Como un valor con dos marcas combinantes va por el camino escalar, la clave dependia de cuantas enies tenia la palabra. La regla vive ahora en un solo lugar y el camino escalar la usa; sobre 7482 cadenas y cinco perfiles los dos caminos coinciden en las 7482. - La sigma final griega se iguala a la sigma al comparar, como ya hacia el pliegue de la proteccion: una palabra griega en mayusculas y la misma en minusculas no se igualaban en detectar_claves() ni en detectar_duplicados_aproximados(). convertir_minusculas no la toca: una sigma final bien escrita sigue igual. - perfilar() cuenta n_distintos_normalizados como detectar_claves() con la marca bytes. El perfil leia como texto lo declarado bytes y las otras dos funciones no: sobre Cafe Central, el mismo con tilde declarado bytes y CAFE CENTRAL, el perfil publicaba 1 y detectar_claves() 2 en un campo con el mismo nombre. Comparten ahora la misma clave. - Una celda irreparable ya no bloquea la reparacion de las demas, y una reparacion parcial se selecciona marcando aplicar. El plan decidia "parcial" con el estado de toda la columna, y una celda con el caracter de reemplazo junto a tres reparables dejaba la accion informativa; y una accion informativa no se puede aplicar, asi que la "seleccion explicita" que pide la ayuda abortaba -la prueba del paquete la activaba reescribiendo estado-. Ahora la accion repara lo reparable y la justificacion cuenta lo que queda como esta; con una celda reparada solo en parte queda lista y sin activar, y aplicar = TRUE la ejecuta con las parciales. - La ayuda escribe dos reglas que el codigo ya seguia: la comilla simple ASCII cierra pero no abre (?normalizacion), y un texto legitimo con los mismos bytes que un mojibake se acusa y se reescribe si se aplica la reparacion (?planificar_limpieza). - El texto sin marca se declara antes de la primera expresion regular, no despues. Bajo LC_CTYPE=C, perfilar() avisaba dos veces "unable to translate ... to UTF-8" sobre una columna UTF-8 sin marca: la proteccion pasaba trimws() sobre los valores antes de declararlos, y con options(warn = 2) el aviso se volvia error y dejaba sin aplicar el filtro del catalogo de ausencias. En el mismo lugar, nchar() contaba bytes y una palabra corta con tilde pasaba el piso de seis caracteres solo bajo C. Lo mismo en recortar_espacios, reemplazar_separadores, decodificar_entidades_html y los validadores: validar_ci_uy() daba FALSE bajo C para una cedula con un espacio duro que en una sesion UTF-8 daba TRUE. Una enumeracion por programa de las 656 llamadas de la familia de expresiones regulares sin useBytes encontro esos cuatro lugares entre las que reciben texto del usuario; ahora todas pasan por la misma declaracion. Los caminos de perfilar_dbi() y perfilar_por(): - bloque_filas publica las cifras exactas de una columna integer64 y no trunca las de las demas. El controlador entrega como integer64 todo BIGINT de PostgreSQL -bigserial, la clave por omision- y todo INTEGER de SQLite que no entra en 32 bits -marcas de tiempo en milisegundos, cedulas-, y el recorrido por bloques le aplicaba min(), max(), mean(), order() y median() de base, que leen sus bits como un doble: minimo = 4.94e-324 en la clave, NaN en una columna con negativos, y como la media integer64 entraba primera en resumen_tabla$columnas, la columna media y la mediana de toda la tabla pasaban a integer64 y truncaban las de las columnas dobles -0,5604 se publicaba 0-, todo con estado calculado. Ahora rige la regla de 2^53 que ?perfilar_dbi promete para el agregado SQL: por debajo se opera con el valor exacto y las cifras coinciden con las de la via sin bloque_filas (SQLite y PostgreSQL, bloques de 10 y de 7); desde 2^53 las siete metricas de magnitud quedan no_disponible con su motivo, y la moda y n_distintos se siguen contando sobre el valor exacto. La frontera se escribe una vez y la usa tambien perfilar() en memoria. Y ninguna columna cambia la clase de un campo de la tabla: las magnitudes de cada fila se convierten a doble antes de unir las filas, en las dos vias. - perfilar_por() mide la columna entera dentro del universo declarado. El centinela, la numeracion densa y la guarda de identificador de "la columna entera" se calculaban sobre todos los valores, sin la mascara de aplicabilidad que perfilar() aplica: con aplicabilidad = list(ingreso = ~ edad >= 18), el relleno 999999 de los menores ganaba la conjetura, el 88888 del universo -el que perfilar() publica, con 18 afectados- se declaraba "de la particion" y desaparecia de los tres grupos. Y una numeracion densa cuya moda sobresale -50 veces 999 en una serie de 2000- se daba por numeracion limpia: perfilar() publica los 50 faltantes_disfrazados y perfilar_por() los movia a la cobertura. Solo es de la particion lo que la columna entera no conjetura. - La corroboracion compara valores. Dos textos iguales son iguales: la moda "3,75" de una columna de texto que el perfil infiere doble, y cualquier moda decimal con OutDec = "," -que el paquete conserva a proposito-, se leian como numero, daban NA en los dos lados y salian divergencia, que subia a perfilar_coleccion() como "El perfil interno declaro 2 divergencias". El numero publicado se lee con la marca de OutDec. - La moda numerica se escribe igual por los tres caminos, con todas sus cifras. La via por bloques publicaba "1e-20" y "1.234e-05" donde perfilar() y el agregado publican "0.00000000000000000001" y "0.00001234", y comparar_equivalencia() declaraba distinto el mismo valor. Y la moda se escribia con 15 cifras: 0.1 + 0.2 salia "0.3" y 1/3 "0.333333333333333", que no vuelven al valor, y dos tablas con moda 0.3 y 0.1 + 0.2 se daban por identicas en el mismo par donde el minimo salia distinto. Ahora se usan las cifras que hacen falta para volver -15, 16 o 17-, en notacion fija y con la marca de OutDec, como promete ?perfilar. Un perfil guardado antes tiene esas modas con 15 cifras: comparar_equivalencia() contra uno nuevo las ve distintas, y lo son. - bloque_filas declara su criterio, y la corroboracion no llama divergencia a lo que el criterio anticipa. Sobre una columna con cotejamiento insensible a caja, la misma tabla daba n_distintos 5 y moda "Montevideo" sin bloque_filas y 9 y "Rivera" con el, y solo el agregado decia por que. Ahora el resumen por bloques publica su meta$criterio_comparacion y su meta$criterio_moda -compara en R, byte a byte-; y la corroboracion del agregado deja en una fila no_corroborada la diferencia que el cotejamiento explica -menos distintos en el resumen SQL, o una moda empatada desempatada distinto-, en vez de tres divergencia. Al reves sigue siendo divergencia. La mediana de una columna de texto por bloques es no_aplica, como en el agregado. - instrumentar = TRUE deja de costar diez veces la medicion, y atribuye solo lo que ocurrio en la ventana. Con unas 4.900 filas en pg_stat_statements, una tabla de una columna y 23 filas tardaba 53 a 58 s contra 0,1 s sin instrumentar: el normalizador de SQL era cuadratico, se corria sobre todo el catalogo dos veces y sobre el SQL de la corrida una vez por consulta medida. Y con un solo queryid repetido en el catalogo -la misma consulta de otro usuario- ninguna fila se emparejaba, y la historia entera del servidor se publicaba como de esta corrida: 726 consultas "observadas" y 5459 llamadas donde hubo 3. Ahora el catalogo se lee limitado al usuario y la base de la conexion, se normaliza solo lo que la ventana movio, una entrada repetida no se atribuye, y la misma tabla tarda 0,3 s contra 0,15 s. - Lo que la proteccion tapa en resumen_tabla se declara. Con bloque_muestra = "solo_agregados" y la proteccion por omision, la moda, el minimo, el maximo, la media y la mediana de todas las columnas se tapaban con estado = calculado y sin motivo. Ahora su fila de resumen_tabla$sql dice omitido_por_privacidad, con el motivo -columna personal, o cerrada porque sin muestra no se pudo clasificar-, por todas las vias. El desvio se sigue publicando, como en perfilar(). Ronda 28: las geometrias, la comparacion de perfiles, los diagnosticos numericos y la proteccion otra vez Cuatro refutaciones. La de las geometrias encontro doce defectos -con un universo declarado, el perfil contaba como evaluadas geometrias que nunca evaluo y publicaba 0 invalidas donde habia 50-; la de la comparacion de perfiles, ocho -una columna protegida cuyo rango se corria setenta veces su ancho no producia ninguna fila-; la de los diagnosticos numericos, diez -una edad de 18 a 90 salia como identificador y Benford se callaba con muchas filas-; y la de la proteccion, nueve -el mensaje de error del motor citaba la celda que lo provocaba-. La proteccion: - El mensaje de un error del motor se publica sin los literales que cita. Una vista que castea una cedula escrita con puntos hacia fallar la consulta, y PostgreSQL o duckdb citaban la celda -invalid input syntax for type bigint: "4.685.467-9"-; ese valor nunca se leyo, el piso no lo conocia, y perfilar_dbi() lo publicaba en resumen_tabla$sql$motivo, cobertura$motivo, meta$proteccion_personal$fuera_de_muestra y el motivo de las columnas omitidas -seis veces en duckdb, una por consulta-, y perfilar_coleccion() en cobertura_metricas, meta$lecturas y cobertura_coleccion, tambien con conservar_perfiles = FALSE. Todo mensaje de error atrapado en la via DBI pasa ahora por un solo lugar: los literales entre comillas -simples, dobles o tipograficas- salen [literal omitido], con o sin proteccion, salvo una palabra de SQL -Incorrect syntax near 'LIMIT' dice que el dialecto no sirve- o un signo suelto; y el contexto con que PostgreSQL cita, sin comillas, el texto que no pudo leer como JSON. - La copia de analizar(conservar_datos = TRUE) aplica el piso en todo el data.frame, no solo en las celdas de texto y de numeros: los nombres de fila -las ciento veinte cedulas de rownames(d) <- d$cedula, que vuelven a ser numeros-, las etiquetas de valor de una columna protegida haven_labelled -que conserva solo los atributos de su clase-, una copia POSIXct de la fecha con hora protegida -que perfilar() si tapaba-, las columnas de lista y los atributos de la tabla y de cada columna. Es una regla recursiva: cada valor, donde este, pasa por el piso. - Las acciones de ausencias y de centinelas ejecutan lo que el plan estima. Con un marcador del catalogo (sin dato) en una columna protegida, o uno declarado en cadenas_ausencia que aparecia tambien en una protegida, la accion convertir_ausencias_textuales salia lista, activa, con valores = "[valor protegido]", y aplicar() la registraba fallida. Un marcador del catalogo o declarado no es el dato de nadie y guarda su valor. Y un centinela declarado en sentinelas_numericos entraba al piso siempre y se tapaba a si mismo en meta, de donde el plan toma la lista: la accion de un monto salia valores = -999, NA y fallida, tambien sin un solo 99999999 en la columna protegida. Con los datos, entra al piso solo si aparece en una columna protegida -tambien como su relleno-; y si aparece -el 99999999 que se repite en la cedula de una tabla de movimientos-, la accion de cualquier columna lo declara (valores_enmascarados) y lo resuelve con los rellenos de la columna, o queda bloqueada, como ya hacia sobre una columna protegida. La prueba que compara los parametros ejecutables con la corrida sin proteccion tiene ahora en su tabla marcadores y un centinela declarado; el barrido de analizar() sobre su plan volvia a tapar los marcadores que el plan ya habia devuelto. - El celular con prefijo de pais pegado a otro numero por una barra o un guion se reconoce: en "+59899123456/+59899765432" el numero tiene dos grupos, el primero no es 598 y la parte nacional no se probaba; los cuatro celulares de una lista asi se publicaban en los ejemplos de una observacion -81 celdas en analizar()- mientras con " / " o con ; se tapaban. - Un objeto declarado tipo = "vista" en perfilar_coleccion() se registra como no leido: se salteaba antes de registrarse y el perfil conservado de movimientos publicaba la cedula y el nombre de los clientes, sin que otras_tablas lo dijera. Todo camino que saltea una tabla la registra ahora por el mismo lugar. - El mapa enmascarado de una imputacion se valida sobre los datos que recibe aplicar(): se reconstruia entero sobre ellos, y sobre otra entrega la accion que sin proteccion queda fallida -la fila S02 -> Farmacia Centro Norte, publicada en claro, con otro nombre- salia ejecutada y escribia valores que el plan nunca mostro, e imputaba una sucursal nueva. Las filas en claro son ahora el mapa, y una fila tapada se resuelve con el par que, tapado con los valores protegidos de esos datos, da lo que el plan publico; si los datos la contradicen, o no traen el valor que la tapo, la accion queda fallida. - Un campo que refiere columnas publica la columna aunque mezcle (tabla): en una planilla pivoteada con la docente protegida y una columna por docente, medicion$objeto_medible y tablero$objeto decian [valor protegido] donde nombraban la columna, y el origen de la propuesta dependencia_funcional:[valor protegido]->[valor protegido]. - Toda forma de compatibilidad de un signo ASCII se lee como el signo en el correo y en la fecha con hora: los parentesis superindice y subindice, las formas de presentacion vertical y el punto de guia faltaban, y juan.perezatempresa.com.uy se publicaba frente al correo protegido. La lista se genero recorriendo Unicode: 28 caracteres que NFKC lleva a un solo signo ASCII y no se leian. Una letra encerrada en un circulo no es un signo y no se lee; queda escrito en ?perfilar. - La cifra del fijo que se tapa de mas se corrige con lo medido. ?perfilar decia "entre tres y seis de cada cien, igual con su prefijo de pais o pegado", y el reproductor de la misma version imprimia 5,5 % sin el prefijo, 2,8 % con el prefijo y espacios y 1,5 % pegado. Las familias del reproductor sortean 400 fijos por escritura, cada una con sus cifras: la diferencia es el sorteo. Con las MISMAS 4.000 cifras escritas de seis formas -con espacios, con guiones, con el prefijo y espacios o guiones, pegado con el prefijo y sin el- se tapan 2,8 % en todas, y exactamente los mismos 112: la regla prueba la parte nacional. El bloque esta al final de data-raw/medir_tapado_de_mas.R, con su semilla, junto con las listas de dos telefonos unidos por una barra (5,5 %; con " / ", 7,8 %, sobre 400). Las geometrias: - Con un universo aplicable declarado, n_validez_evaluados y n_dominio_evaluados cuentan solo lo que de verdad se evaluo dentro de el: antes se derivaban de las filas aplicables, y las geometrias que el presupuesto no analizo, los NA de una columna de texto y -para el dominio- las vacias se publicaban como evaluadas. Con los topes por omision, 50 poligonos invalidos del universo, todos fuera del presupuesto, salian "0 invalidas de 120 evaluadas" y geometria_invalida no se emitia; ahora salen "0 de 20" y cobertura_diagnosticos agrega cuantas del universo quedaron sin analizar ("se analizaron 20 de sus 120 valores presentes"). - geometria_vacia y tipos_geometria_mixtos cuentan sobre las geometrias medidas del universo, no sobre las filas de la tabla: 2 vacias "de 10" sobre un universo de 6, 1 "de 10" sobre 7 WKT presentes, 3000 sobre 2925 geometrias de PostGIS. tipos_geometria_mixtos dejaba adentro las filas donde la regla de aplicabilidad quedo indeterminada ("cuenta 5 y traza 3") y se emitia con afectadas sobre un universo vacio; ya no. - Un valor que no convierte ya no borra la medicion de la columna ni se calla: un solo "", "zz" o un encabezado trunco dejaba una columna EWKB hexadecimal sin representacion, sin motivo y sin un campo geometrico; en WKB crudo el valor perdido no se declaraba; en WKT uno solo dejaba la columna entera sin medir. Ahora se mide lo que convierte y lo demas se declara con su cifra en motivo_representacion y en cobertura_diagnosticos. El WKB hexadecimal tiene su propia guarda de encabezado -la misma aritmetica que el crudo- y solo lo que la supera llega a sf; si una llamada falla, se biseca. - La guarda WKT admite lo que ST_AsText de PostGIS escribe: GEOMETRYCOLLECTION Z (POINT Z (1 2 3)), la variante M y las colecciones anidadas salian como "WKT estructuralmente invalido". Las curvas siguen sin medirse y se declaran como no convertidas. Los parentesis se verifican balanceados de verdad (contar aperturas y cierres dejaba pasar )(). - Una columna EWKT con varios SRID, o con valores con y sin SRID, se mide por SRID como la misma columna en EWKB: antes sf la rechazaba entera y ademas se emitia crs_no_declarado [error]. Si ningun valor convierte pero los valores declaran su SRID, crs_declarado lo dice y no se acusa crs_no_declarado. - Un componente vacio dentro de una geometria que no es vacia -un POINT EMPTY en una coleccion o en un MULTIPOINT, que sf escribe como NaN NaN- ya no se cuenta como coordenada fuera de dominio. - En un CRS geografico con otro meridiano de origen (Ferro, Paris, Jakarta) o en gradianes, el rango se exige en sus unidades y la comparacion con la BBOX del area de uso se hace despues de llevar las coordenadas a longitud/latitud WGS 84: el centro del area de uso de EPSG:4805 salia fuera de dominio. - Con SRID mezclados, todos geograficos (4326 + 4258), geometria_invalida es sospechoso, igual que con un solo SRID geografico; antes cualquier mezcla la volvia error. - n_geometrias_analizadas y n_vertices_analizados se publican en p$columnas, como ?perfilar prometia: el trabajo del presupuesto geometrico solo aparecia en el texto de la cobertura, y solo cuando habia recorte. - Sobre una geometria escrita como texto, la proximidad de vocabulario, posible_identificador y patron_raro declaran "no aplica" tambien cuando corrieron sin producir hallazgo: con 2.500 WKT la cobertura decia "el vocabulario excede el alcance de comparacion" y con 50, "no aplica". - Con un universo aplicable declarado, el presupuesto geometrico se gasta primero en el universo -con el mismo muestreo uniforme si el universo solo ya excede el tope- y lo que sobra va al resto de la columna; el tope de la conversion de una columna WKT o WKB sigue la misma regla. Antes se repartia entre todas las filas: con los topes por omision, un universo de 120 poligonos que entraba entero en el tope quedaba analizado en 20 mientras se median 980 filas de afuera, y 50 invalidas del universo salian "0 de 20". Ahora salen 50 de 120. Sin universo, el presupuesto hace lo mismo que antes. - La proteccion de datos personales leia representacion_geometrica, un campo que no existe; corregido el nombre, ademas solo declara no_publicado_por_geometria_protegida donde habia una caja que tapar: una geometria de texto que no se pudo convertir conserva bbox_alcance en NA. La comparacion de perfiles: - Una columna protegida declara que su rango no se compara. Con la proteccion por omision, un telefono cuyo rango pasaba a setenta veces su ancho no producia ninguna fila ni ningun aviso: la comparacion no decia el cambio, que no puede publicar, ni que no lo habia mirado. Ahora el resultado lleva siempre el atributo campos_protegidos -columna, campo, lado, la misma forma que ya usaba comparar_equivalencia(), con la misma regla para decidir que esta tapado-, y rbind() une los de todas las derivas. - Un cambio de proteccion de la columna tiene su propia fila. Cien documentos con los mismos extremos, que cruzaban el umbral de verificacion entre una entrega y otra con la misma politica, salian como "Cambio el rango observado", sospechoso, con magnitud Inf. Ahora sale una fila aspecto = "proteccion", sospechoso, sin proteger -> protegido, como configuracion_proteccion una escala mas arriba; si cambia la politica entera, esa fila ya lo dice y no se repite por columna. - Los extremos exactos de un entero que no entra en un doble se comparan. Un integer64 por encima de 2^53, o un entero escrito como texto, publica minimo y maximo en NA y conserva los extremos en minimo_exacto y maximo_exacto, que la comparacion no leia: un ancho que se multiplicaba por cuarenta y nueve daba cero filas. Se restan con aritmetica exacta por cifras, sin bit64. - El rango se decide por sus valores, no por su texto. Se comparaba el rango impreso con ocho cifras, que sigue a options(scipen): los mismos perfiles daban cero filas o una fila sospechoso segun la opcion, y un rango angosto frente a su magnitud -1.000000030 contra 1.000000044- no salia nunca. Los numeros de la deriva se publican ahora con la escritura mas corta que vuelve al valor, sin depender de scipen ni de digits -antes salia [2.1e+11, 2.1e+11] para dos extremos distintos-; la marca decimal sigue a OutDec, y con coma decimal los extremos se separan con punto y coma. - Un patron que entra o sale de un resumen lleno no se publica como nuevo ni como desaparecido. Con mas formas que max_patrones, mover un valor entre dos formas raras publicaba un patron "nuevo" y otro "desaparecido", los dos sospechoso, que seguian en los datos. Ahora sale no_comparable con severidad ok, salvo que el cambio que se puede garantizar alcance el umbral. - El cambio de proporcion de un patron que sigue presente se informa. Un formato que pasaba del 5 % al 95 % no producia ninguna fila. Ahora sale modificado cuando alcanza umbral_cambio (error desde umbral_error); que los cambios menores no den fila es una decision escrita en la ayuda. - comparar_equivalencia() no afirma un cambio de tipo temporal que no midio. El frame de perfilar_dbi() no publica tipo, y la guarda lo leia como "no temporal": fechas que el motor devolvia como Date salian con tipo_cambiado:temporal_vs_no_temporal. Sin tipo de un lado, los mismos campos se omiten con tipo_desconocido:temporal_vs_sin_tipo, porque tampoco hay una unidad comun garantizada. - Un cambio significativo de cardinalidad es sospechoso en los dos sentidos. La regla del signo era la de los faltantes, y un identificador unico que pasaba a tener la mitad repetida salia ok. Es una decision escrita en la ayuda: la cardinalidad no tiene una direccion buena. Benford: - La exclusion por "secuencia correlativa" ya no depende de cuantas filas se cargaron. La guarda contaba cuantos enteros distintos ocupan el rango y no cuanto se repite cada uno, y una magnitud entera acotada termina ocupando todos los de su rango si hay filas suficientes: floor(10^runif(n, 0, 4)) tenia densidad 0,094 con 2.000 filas y 0,97 con 200.000, donde la guarda la daba por numeracion y callaba un 20 % de montos fabricados con X2 = 93.078. Es el defecto por el que el paquete ya habia retirado el criterio de unicidad. La condicion es ahora una conjuncion -densa y casi sin repetir, con la misma unicidad de 0,9 que define el tipo identificador-, que sorteando con reposicion no se cumple a ningun tamano: medido sobre cinco distribuciones y ocho tamanos, la unicidad mas alta entre las columnas densas fue 0,659. Por la misma razon, posible_identificador excluye a Benford solo cuando la columna no se repite. Un correlativo de verdad sigue excluido con cualquier n. El costo esta del lado barato: una numeracion que se repite, como el identificador del hogar en una tabla de personas, puede recibir desviacion_benford. - Los ordenes de magnitud son log10(max/min), como dice la ayuda. El codigo restaba logaritmos, y con 534 y 0,534 -cociente 1000 exacto en doble- daba 2,9999999999999996: Benford no se evaluaba y el motivo publicaba "3.000000000000000 < 3". De 50.000 pares decimales con cociente 1000, la resta dejaba 5.728 por debajo de 3. Ademas el motivo compara ahora los textos que publica, hasta 17 decimales: round() y formatC() no redondean igual, y el valor y el umbral podian escribirse identicos. Centinelas declarados: - Lo que sentinelas_numericos declara como ausencia sale tambien de Benford y de las relaciones aritmeticas, como ya salia del resumen. Con 160 999 declarados en un monto, el resumen publicaba la media sin ellos y Benford contaba 2.000 valores y publicaba desviacion_benford (X2 = 245,4), cuando sin los 999 la columna cumple (X2 = 3,98, p = 0,86); con -9 declarado en ocho filas, a + b ~= total salia sospechoso acusando justo esas ocho. Se aplica la misma regla del resumen, no una reescrita, y la lista por omision sigue sin sacar nada. Relaciones aritmeticas: - Un sumando que la tolerancia absorbe ya no completa una suma. La tolerancia es relativa a la escala del total, y con montos de 1e9 vale 10 a 20 unidades: junto a neto + comision ~= bruto se publicaba [sospechoso] neto + hijos ~= bruto, que acusaba a cinco filas sanas de romper una regla inexistente, y con marcas epoch inicio + intentos ~= fin. Con el criterio del filtro de magnitud de las relaciones de orden: si la identidad sin uno de sus sumandos ya alcanza el criterio, ese sumando esta por debajo de la resolucion y la identidad se descarta, salvo que se cumpla exacta -con la sola diferencia del redondeo de un doble-, como la brecha de IQR cero rescata un orden. La relacion real sale, con la evidencia que lo dice, y las descartadas se cuentan en meta$aritmetica_columnas$identidades_descartadas_escala. - Un desborde de enteros no apaga ni aborta la busqueda. x + y en integer daba NA en una fila y la relacion desaparecia sin fila de cobertura, con el aviso base de enteros excedidos filtrado al usuario; por perfilar_dbi(), RSQLite devuelve INTEGER como integer y un importe en centesimos se callaba entero. Y con un integer de -2e9 a 2e9 el rango desbordaba y perfilar() abortaba. La identidad aditiva y la variacion se calculan en doble, que guarda exacto a cualquier entero. Ausencia estructural: - El corte de un umbral es el limite exacto. Con mas de 200 cambios de valor se prueba un corte cada medio por ciento, y se publicaba el valor siguiente al corte muestreado: sobre una regla exacta >= 50000 se sugeria >= 49849.27, que cumple 0,9988 y pegada dejaba seis faltantes dentro del universo. Ahora el muestreo elige la zona y se prueban todos los cambios entre los dos cortes vecinos: el corte publicado es el primer valor observado del lado donde la columna corresponde. - El corte de un instante lleva su zona. as.POSIXct("2024-03-01") se leia en la zona de la sesion que lo pegaba: con la sesion en UTC ubicaba bien todas las filas y en America/Montevideo dejaba seis presentes fuera del universo. La etiqueta se escribe en la zona de la columna -o en UTC- y la llamada lleva tz. - La sugerencia no depende de OutDec. Con OutDec = "," decia ingreso >= 30122,33, que no es codigo R. El numero se escribe como la etiqueta de un grupo: punto decimal, sin depender de scipen, y con las cifras que hacen falta para volver al valor. La prosa sigue respetando OutDec. - n_afectados cuenta las ausencias que la regla explica: las que caen donde la columna no corresponde, sin las filas donde el determinante falta. Era round(cumplimiento * n_ausentes), que no es un conteo de nada: daba 108 donde la regla explica 100, y 300 donde explica 100 con 200 determinantes faltantes. La evidencia lo dice con las dos cifras. clasificar_variables(): - Una unidad declarada no confirma lo que solo dicen los valores. Un peso en kg con valores enteros salia discreta, confirmada y con confianza 1. La unidad establece una magnitud, no si es una cuenta o una medida: confirma continua cuando hay decimales, y con enteros la propuesta sale sin confirmar. measure = "scale" sigue publicandose como continua confirmada: es una declaracion, y esa es la lectura que SPSS le da al nivel; queda escrito en la ayuda. La numeracion, en todo el perfil: - Una regla de numeracion para todo el perfil. La densidad sin repeticiones no solo callaba Benford: decidia tambien posible_identificador y las guardas de Tukey, de centinelas y de patrones. Una edad de 18 a 90 con 20.000 filas salia "codigo de catalogo, no una magnitud", y una magnitud acotada pasaba a numeracion al crecer la tabla -y ahi se callaban sus limites de Tukey-. Ahora hay una sola regla, escrita una vez y alimentada con los conteos de cada sitio (en memoria, por perfilar_dbi() y en perfilar_por()): densa y casi sin repetir, descontando el excedente de la moda. secuencia_entera_densa pasa a responder lo mismo. Un correlativo y un identificador con huecos siguen siendo numeracion a cualquier tamano; una numeracion que se repite -un codigo de catalogo, una clave foranea- no se distingue por su forma de una magnitud que lleno su rango y deja de tratarse como numeracion, asi que el paquete puede hablar de mas sobre ella. En el banco de trece columnas que decidio la guarda, el codigo 1..284 que se repite pasa a recibir Tukey: acierta doce, y el error queda del lado barato. - La aplicabilidad declarada manda tambien en Benford y en la aritmetica. Las filas fuera del universo -y las indeterminadas- salian del resumen y seguian sosteniendo una desviacion de Benford o apagando una relacion: con 200 de 1.200 filas fuera del universo, Benford publicaba X2 = 356,8 sobre la columna entera cuando sobre el universo da 9,4, y a + b ~= total se perdia porque la basura de afuera bajaba el cumplimiento a 0,83. Se usa la misma mascara que el resumen. - La traza de outliers, ceros y negativos usa el universo declarado. Con aplicabilidad, el conteo salia de los cuartiles del universo y la traza de los de la columna entera, y el paquete se acusaba de una traza incoherente ("cuenta 4 y traza 3"). En enteros lo tapaba la guarda de densidad. - Los numeros que tienen que volver a su valor no dependen de LC_NUMERIC. Con Sys.setlocale("LC_NUMERIC", "de_DE.UTF-8"), as.character() y sprintf() escriben coma aun con OutDec = ".": las etiquetas de grupo de perfilar_por() salian "1,5", la comprobacion avisaba "NAs introducidos por coercion" y 0.1 + 0.2 quedaba "0.3". La escritura reversible devuelve el punto en un solo lugar, el mismo que ya usaba la sugerencia de ausencia estructural. Ronda 27: la proteccion otra vez, los validadores y las fechas, los duplicados y la distribucion Tres refutaciones en paralelo. La de la proteccion encontro siete fugas, tres familias de tapado de mas, un aviso que con warn = 2 abortaba perfilar() y un aborto con un nombre de columna marcado bytes -la marca de una columna protegida volvia a llegar tapada a los datos del usuario por otra puerta-; la de los validadores y las fechas, doce defectos -el perfil declaraba la coma decimal y resumia 1.500 como 1,5-; la de los duplicados, la distribucion, las asociaciones y las estimaciones, trece. La proteccion: - La marca .ausente_ de una columna PROTEGIDA ya no sale [valor protegido]: con "Segundo" -un nombre de pila- protegido en Segundo Nombre, la accion recomendada marcar_filas_ausentes llevaba columna_marca = "[valor protegido]" y aplicar() creaba en los datos una columna con ese nombre; el dependiente de la imputacion por dependencia salia tapado igual y la accion quedaba fallida. El barrido por columna de los parametros no recibia los nombres de la entrada como intocables (la ronda 26 los habia pasado solo al barrido de todo el plan). Tambien soporte_minimo y cumplimiento de la imputacion se tapaban cuando coincidian con un valor de una columna protegida -id_persona = 1..40- y la accion quedaba fallida. Una prueba recorre ahora todos los parametros del plan y los nombres de aplicar()$datos en la llamada por omision y los compara con la corrida sin proteccion. - El mapa de una imputacion por dependencia que la proteccion toca por contenido -una sucursal que se llama como un titular protegido- queda marcado mapa_enmascarado = TRUE y aplicar() reconstruye la relacion sobre los datos. Antes la accion lista quedaba fallida si lo tapado era el valor, o ejecutada con la mitad de las celdas y sin aviso si era la clave. - perfilar_coleccion(conservar_perfiles = TRUE) cierra del lado seguro cuando una tabla de la coleccion no se puede perfilar -un rol sin permiso sobre clientes-: sus valores protegidos son desconocidos, asi que en los perfiles de las demas se tapan los valores y la evidencia de las columnas no personales, y otras_tablas lo dice. Antes la tabla ilegible no se registraba y el perfil de movimientos publicaba la cedula y el nombre del cliente que su observacion citaba. - perfilar_dbi(universo = "muestra_motor") ya no confirma un relleno sobre la muestra del motor: ahi los conteos calculado son los de la relacion muestreada, y la cedula valida de un cliente frecuente era "relleno" en la muestra y salia en la moda, los ejemplos y la evidencia de otra columna. Sin tabla entera que lo confirme, el relleno sigue en el piso. - En perfilar_dbi() con muestreo, un relleno confirmado sale del piso solo en su columna, como en memoria: el 1.111.111-1 de relleno de cedula que es la cedula de un conyuge en ci_conyuge se sigue tapando en las demas columnas. Antes se sacaba de los valores de todas las columnas protegidas. - Un valor protegido igual a un nombre de columna -o a uno que el paquete deriva, como el snake_case- ya no se publica en los campos de valores: en una planilla pivoteada con una columna de notas por docente y la docente protegida, patrones$aprobo$ejemplos, niveles_observados, la configuracion propuesta de analizar() y el informe HTML publicaban los nombres de las docentes, tambien en perfilar_dbi() y entre tablas en perfilar_coleccion(). El barrido salteaba todo campo hecho de nombres; ahora un campo hecho de nombres que son valores protegidos se barre, salvo los que refieren columnas (columna, determinante, nombres_propuestos...), donde el nombre es estructura. - analizar(conservar_datos = TRUE) aplica el piso tambien a las demas columnas de la copia: tapaba la columna cedula y dejaba las cedulas en obs ("pago CI 3.738.538-0") o guardadas como numero en otra columna. - Con texto latin1 sin marca -el que deja read.csv() sin declarar la codificacion- la lectura de nombres del barrido avisaba "input string 1 is invalid UTF-8" y, con options(warn = 2), perfilar() abortaba. Corta por bytes, como el resto del barrido. - Un nombre de columna marcado bytes ya no hace abortar analizar(), planificar_limpieza() ni la imputacion por dependencia de aplicar(), con y sin proteccion ("no se permite traduccion de cadenas con "bytes""): esas rutas buscaban la columna por su nombre, que R no traduce, y ahora la buscan por su posicion. - El telefono uruguayo con su prefijo de pais se reconoce como telefono: su parte nacional se prueba como si estuviera escrita sola y el prefijo no se une a otro grupo. Pegado (E.164) -"wa.me/59899123456", "+59899123456", como lo escriben los enlaces de WhatsApp- se publicaba frente al celular protegido "099 123 456", y al reves; separado, "598" y el primer grupo formaban una cedula de siete cifras sin su verificador y un fijo de cada seis que no era de nadie se tapaba ("+598 4499 0917": 16,8 % medido; ahora 2,8 %, lo mismo que el fijo escrito sin el prefijo o pegado: medido sobre las mismas 4.000 cifras escritas de seis formas, la regla tapa los mismos 112 en todas. El 5,5 % que el reproductor imprimia sin el prefijo era el sorteo de 400 fijos con otras cifras, semilla 7). - El correo escrito con palabras y la fecha con hora se leen tambien con las demas formas de compatibilidad de un signo ASCII, las que la regla de digitos ya leia: los parentesis blancos anchos (U+FF5F, U+FF60), el punto ideografico y el de medio ancho, las variantes chicas de la coma, el punto, los parentesis o la arroba (U+FE50 a U+FE6B), la barra partida y el salto de linea de C1. "juan.perez(at)empresacomuy" o "2024-06-0709:04:05" se publicaban frente al valor protegido. Los validadores, los tipos y las fechas: - El resumen de los numeros escritos como texto lee con la convencion que el perfil publica. Una columna con numero_texto_convencion = "decimal_coma" resumia 1.500 como 1,5 con un sub(",", ".") propio, mientras la limpieza del mismo paquete convertia el mismo texto en 1500: media 5.0994 donde a mano da 4849.65, y un hallazgo outliers sobre importes ordinarios. El resumen, la secuencia entera, los faltantes disfrazados, los outliers y los limites de Tukey del plan leen ahora por la misma normalizacion que convertir_numero_regional, en perfilar() y en la muestra de perfilar_dbi(). Lo que la convencion no puede leer queda afuera y se declara (n_valores_excluidos_resumen, calculados_sobre_valores). - Un separador seguido de tres cifras ya no se decide por la otra. 1.500 dejaba de ser numero solo si no habia ninguna coma en la columna, y 1,500 no tenia guarda: la columna de miles con coma salia doble con su resumen leido como decimales, y la gemela con punto salia texto. inferir_tipo() cuenta esas formas como numero solo si la columna aporta evidencia de una sola convencion -la misma evidencia que publica numero_texto_convencion-, para los dos separadores por igual. - %y-%m-%d es otra lectura de 01-02-03, no otro formato. Fuera del grupo ambiguo, una columna homogenea dd-dd-dd recibia formatos_fecha_mixtos con severidad error, detectar_formatos_fecha() publicaba n_inequivocos = n para valores con tres lecturas, y formato_fecha_ambiguo omitia la tercera (o no salia, con 25-12-31). Ahora las tres lecturas son candidatas de una sola representacion. Y dos lecturas probadas cada una por valores propios son formatos mixtos tambien con ano de dos cifras (13/01/20 y 01/13/20), como ya lo eran con cuatro. - May toma la etiqueta de su columna. Es el nombre completo en ingles y la abreviatura de mayo; etiquetado siempre %b, una columna inglesa de meses completos con un mayo recibia formatos_fecha_mixtos (error). - La hora 24:00 no se acepta, por el mismo motivo que el segundo 60: strptime() la normaliza al dia siguiente y 2024-01-31 24:00 se publicaba como el 1 de febrero. El valor queda fuera del resumen y se declara. - El texto con desplazamiento horario declara que no se evaluo la fecha civil. 2020-01-31T23:30:00-03:00 se publica como 2020-02-01 02:30:00 UTC y el conteo n_filas_fecha_civil_distinta_utc, que es de las columnas POSIXt, quedaba en NA sin fila en cobertura_diagnosticos. - validar_rut_uy() completa con cero el RUT de once digitos, igual que validar_ci_uy() la cedula de siete: un RUT con prefijo 01 a 09 leido por read.csv() llega como numero sin su cero, y la mitad de la columna se rechazaba; perfilar() la bajaba de verificado a alto. - Los validadores recortan cualquier espacio al borde, tambien el espacio duro (U+00A0) que dejan planillas y paginas web: con el, una cedula valida era invalida mientras con un tabulador no. En la cedula y el RUT, cualquier espacio separa grupos. - Un doble desde 2^53 devuelve FALSE en los validadores: no conserva sus digitos, y se validaba la impresion de R (validar_luhn() daba TRUE en uno de cada diez numeros de 19 cifras). El mismo numero como texto o integer64 se valida. - Una fecha, una duracion o una magnitud con unidad devuelve FALSE (o NA) en los validadores: Date, POSIXct y units abortaban, y difftime y hms validaban sus segundos como cedula. - validar_url() exige la IPv4 embebida al final de la IPv6 y con octetos decimales: [1.2.3.4::], [::1.2.3.4.] y [::01.2.3.4] pasaban. - Una letra suelta sin su opuesto no hace logica a una columna. T, F, S y N cuentan como literal logico solo junto a su opuesto: una columna de F y M salia logico, con un tipo_declarado_distinto falso. Las senales redundantes: - Una fila con el mismo infinito en todas las columnas concuerda: detectar_discordancias() restaba Inf - Inf, que es NaN, y con el max_ejemplos por omision abortaba con "subindice fuera de los limites"; con max_ejemplos = 0 contaba la fila como discordante -3 de 5 donde hay 2-. - La comparacion no numerica es de valores, no de su impresion: el mismo instante guardado en UTC y en otra zona salia discordante en todas las filas, porque se comparaba as.character(), que escribe cada fecha-hora en la zona de su columna. Ahora una fecha-hora se compara por su instante -escrito en UTC, como en detectar_relaciones()-, un numero con todas sus cifras -1e5 y el texto "100000" concuerdan; 0.1 + 0.2 y 0.3 no- y, si todas las columnas son duraciones, en segundos. El texto sigue comparandose por sus bytes. - La evidencia no cita iguales dos valores que el veredicto separo: la ronda 15 lo cerro para los dobles; dos instantes a 0,3 s se citaban los dos 12:00:00 UTC, y una palabra con la tilde compuesta y la descompuesta, igual. Esa fila los cita ahora con la escritura que los distingue -el instante con su fraccion, el texto con y la nota de escapes-. Duplicados aproximados: - La barra invertida se escapa una vez: la clave de bytes se aplicaba dos veces por valor y cada \ llegaba cuadruplicada a stringdist: x\y contra x\z publicaba 0,067 y entraba al umbral por omision. La receta del manual dice ahora los dos escapes -\ como \\ y | como \|- y la distancia publicada sale de ella. - Dos dobles que coinciden en 15 cifras no son el mismo valor: 0.1 + 0.2 contra 0.3, o 1e15 contra 1e15 + 1, salian tipo_par = "exacto" a distancia 0 con la misma evidencia. Un doble entra a la comparacion, a la clasificacion y a la evidencia con la escritura mas corta que vuelve a su valor. - NA contra cadena vacia no se atribuye a la normalizacion: con normalizar = FALSE el par salia exacto_normalizado con igualo_normalizar = TRUE. Los dos entran a la comparacion como texto vacio, y ahora la clasificacion los trata igual. - Una fila hecha solo de blancos no entra a la comparacion: " ", " " y "\t" pasaban el filtro de filas vacias y salian como tres pares exacto_normalizado entre si. El criterio es el de n_blancos en perfilar(). - Sin stringdist, los conteos no medidos son NA: alcance publicaba n_filas_validas, n_pares_hallados y los conteos por tipo en 0. Lo mismo cuando la comparacion no corre por el largo de un valor, por nombres de columna repetidos o por no haber columnas comparables. Distribucion y asociaciones: - distribucion_valores() cuenta dobles por valor: convertia la columna a texto con 15 cifras y fundia 0.1 + 0.2 con 0.3 -frecuencia 3 donde hay 2- y 1e15 con 1e15 + 1, mientras perfilar() contaba bien sobre los mismos datos, y analizar() publicaba la cifra equivocada. Cada fila se publica con las cifras que la devuelven a su valor. - Un empate en distribucion_valores() no depende del orden de las filas: se desempataba por primera aparicion, y con max_valores el valor publicado cambiaba al reordenar el archivo. Ahora por valor, el mismo desempate que la moda de perfilar(). - Eta cuadrado no se publica con un solo grupo: con la categorica en un solo nivel en las filas completas devolvia 0 y el par salia como "no hay asociacion"; Cramer, en la misma situacion, lo declaraba no medible. Ahora los dos van a pares_omitidos_medicion. Estimaciones externas: - Dos tabulados de la misma entidad no comparten id_medida: el id se armaba con el numero de fila de la llamada, y evaluar() rechazaba la union de dos tabulados con la misma fecha como una medicion unida consigo misma. Ahora nombra la metrica y la celda estimada, como medir() desde la ronda 23. - La salida es una medicion: su clase era solo medicion_calidad, y historico_calidad() la rechazaba. Ahora es c("medicion_calidad", "medicion", "data.frame"). - Una celda infinita es un numero fuera de dominio: se descartaba con motivo no_numerica, que manda a buscar un texto mal escrito. Ronda 26: la proteccion, el informe HTML y el plan de limpieza Tres refutaciones en paralelo. La de la proteccion encontro nueve fugas y cuatro familias de tapado de mas -una de ellas renombraba la columna de los datos del usuario con el texto tapado-; la del informe HTML, diez defectos; la del plan de limpieza, catorce. La proteccion: - El nombre que el plan propone ya no pasa por el barrido: con "Segundo" -un nombre de pila- protegido en Primer Nombre, la evidencia decia "Segundo Nombre" -> "[valor protegido].Nombre", planificar_limpieza() y analizar()$plan proponian ese nombre en nombres_propuestos -y [valor protegido] en snake_case- y aplicar() se lo ponia a la columna en los datos del usuario, en la llamada por omision. Lo mismo las marcas que crean sus acciones, .ausente_ y .outlier_. Los nombres que el paquete deriva de un nombre de columna son estructura como el nombre del que salen, en todas las salidas. - detectar_dependencias() protege los datos personales: publicaba en evidencia y en print() la cedula y el nombre de una columna personal -536 apariciones en una tabla de movimientos- mientras perfilar() sobre la misma tabla decia [evidencia protegida]. Es el defecto que la ronda 25 cerro en clasificar_variables() y detectar_discordancias(), con la misma regla y los mismos argumentos nuevos, proteger_datos_personales y columnas_personales. Recorridas las demas exportadas que publican valores de celdas, sobre una tabla con columnas personales: solo analizar_tiempo() sin perfil publicaba algo -el minimo y el maximo de una fecha_nacimiento-, y ahora clasifica como clasificar_variables() y protege igual, con los mismos dos argumentos. - En perfilar_dbi() con muestra, el relleno lo confirma la tabla entera: la regla de la ronda 25 -el digito repetido que se repite cinco veces o mas en una columna casi clave sale del piso- se decidia sobre la MUESTRA, donde los clientes que se repiten casi no se repiten, y la cedula valida de un cliente con 600 movimientos salia como moda de otra columna. Ahora el candidato de la muestra se confirma con los conteos exactos de la tabla, y sin ellos sigue en el piso. - El correo escrito con palabras se lee en mas formas: con la arroba escrita y el punto en palabras (juan@gmail punto com), con espacios duros o ideograficos, con los parentesis o la arroba de ancho completo, y dentro de un texto latin1 con una tilde -como lo deja read.csv(encoding = "latin1")-. Se publicaban. Sin costo medido: los no protegidos escritos asi se tapan lo mismo que con la arroba comun, y ninguna de 3.015 frases con esas palabras. La ayuda dice ahora que ofuscaciones no se leen. - La fecha con hora se reconoce con la arroba, el punto y coma o la barra vertical de ancho completo entre la fecha y la hora, como sus hermanas anchas: todo signo ASCII de ancho completo se lee como el de ASCII. - Las letras modificadoras que hacen de acento o apostrofo se pliegan: el nombre protegido escrito con el circunflejo bajo U+A788, el apostrofo U+02BC, la prima U+02B9 o la tilde vertical U+2E2F en lugar de la tilde se publicaba -doce de cuarenta y cinco con nombre de acento-. Pegadas a una letra; entre cifras la prima sigue sin unir un numero. - El celular con su prefijo de pais agrupado de a tres se tapa: 598.099.123.456, con comas o con apostrofos, se probaba solo entero como un importe y se publicaba. Ahora tambien sin su primer grupo. Cuesta uno o dos de cada mil importes de cuatro grupos -su cola coincide con un documento cuando empieza con un cero-, medido sobre 10.000 por familia; antes, cero. - perfilar_coleccion() protege cada tabla con los valores de las otras: con conservar_perfiles = TRUE, el perfil de movimientos publicaba en su moda y sus ejemplos la cedula y el nombre que el de clientes tapaba -una observacion que cita al cliente-. La coleccion es un objeto: cada perfil conservado se barre con los valores protegidos de las demas tablas, leidos de la base con sus frecuencias, y si la lectura falla -o una tabla no se pudo clasificar- se cierra y se dice. perfilar_dbi() sobre una tabla sola sigue protegiendo solo con los suyos, y la ayuda lo dice. Lo que se tapaba de mas: - La columna con clasificacion debil tapa sus valores, no su evidencia: la ayuda decia que lo debil "se informa pero no se ocultan valores", y la evidencia de todos sus hallazgos salia [evidencia protegida] -tambien la que no cita ningun valor, "3 valores", "N/D (30)", "4939 valores distintos de 5000 (0.988)"-, igual que su magnitud de Benford y el log10(max/min) de su cobertura. Ahora se tapan los valores de la columna en la evidencia, con el piso, y la magnitud se publica: su minimo y su maximo ya se publicaban. La forma compatible con un documento que no se pudo verificar se sigue protegiendo entera, como decidio la seccion "Privacidad: ante la duda se protege". - La SQL guardada cita el nombre de columna igual a un valor protegido: con una columna llamada como el titular protegido salvo la tilde, perfilar_dbi() tapaba enteras las 33 sentencias de su lote -que citan tambien las demas columnas- y meta$sql_esquema, mientras la SQL de la muestra lo citaba igual. En la SQL un nombre citado es un identificador. - El rango de redes no se tapa: 10.4.1.0/24-10.4.9.0/24 pegaba la mascara a la IP siguiente por el guion, y uno de cada cuatro (26,8 %) se tapaba por sus tramos; ahora, como la IP con mascara, menos de uno de cada cien (0,8 %). - Declarado con su cifra lo que se tapa por el mecanismo del parcial: la version de cuatro partes v4.62.660.4361, trece de cada cien, y el expediente exp 2023-6-880041, uno de cada seis. Las familias nuevas estan en data-raw/medir_tapado_de_mas.R. El informe HTML: - Las cuatro coberturas de frontera llegan al informe y a la consola: solo se leia la de la coleccion, asi que cobertura_organizacion y las de los dos conjuntos no las publicaban reportar(), print() de la medicion, de la evaluacion, del tablero ni del indice, y el tablero y el indice no las conservaban. Como la organizacion hereda las coberturas de las colecciones que si entraron, el informe de un organismo al que le falto una coleccion publicaba "4 de 4 tablas, ninguna sin medir": lo no medido como medido. El historico del mismo objeto si la registraba. Ahora un solo lector reune las cuatro -directas y heredadas- y cada una sale con sus partes declaradas, las que entraron, las que no y su motivo, y las partes con peso cero nombradas. - Una medida suprimida por una evaluacion no la publica otra evaluacion del mismo informe: la medicion, el historico y el tablero se enmascaraban con las supresiones de todo el informe y la evaluacion solo con las suyas, asi que en reportar(ev1, ev2) la medida que ev1 manda no publicar salia en las medidas de ev2 -"no cumple x > 0.5", que en una medida booleana es el valor-. Ahora la supresion de una vale para todas, en cualquier orden, y la que un historico del informe ya trae suprimida tambien -reportar(historico_calidad(ev1), med) publicaba el valor en la medicion-. Y el enmascarado ya no escribe las demas celdas con as.character(): salian "TRUE" donde el informe dice "si". - Las cifras del informe vuelven al valor del objeto: se escribian con quince cifras, y 0.1 + 0.2 salia 0.3 al lado de "no cumple x <= 0.3", y 2/3 salia 0.666666666666667, otro doble. Ahora llevan las que hacen falta -dieciseis o diecisiete- con la regla de la etiqueta de perfilar_por(), que vive en un solo lugar. - Una cifra no se abrevia como un texto: sin exponente, un numero de |x| >= 1e240 o menor que 1e-238 pasaba los 240 caracteres y el corte de los textos publicaba otra magnitud: el ausente de Stata, 8.988e307, salia como 239 cifras que se leen 8.988e238, igual que su media cien veces menor. Ahora la que no cabe se escribe con exponente y las mismas cifras, y una celda con varias se abrevia por cifras enteras. - "Resumen general" y "Alcance y recortes" escriben cada valor con su tipo: se armaban con c(), y salia "Celdas 1e+05" debajo de "Filas 10000", "Muestreado 0" donde el resto del informe dice "no", y la memoria con el redondeo de la consola; ahora sale en bytes, con la lectura al lado. - La evolucion del historico dice por que falta un delta cuando la deriva no arma el par: dos corridas del mismo perfil sobre tablas distintas salian en serie -0,75 y 1- con el delta, la corrida de referencia y la comparacion vacios, y se leian como una mejora; comparar_evaluaciones() decia "no miden la misma tabla". El motivo vive ahora en el lector unico del par, y los dos lo publican igual. - El informe de un plan publica los diagnosticos que el perfil no evaluo: de las tres declaraciones de lo que el plan no cubre, cobertura_diagnosticos se quedaba en la consola, y un plan mandado solo se leia como "lo demas esta bien". - El historico publica aparte, y sin tope, lo que no se midio: una metrica sin valores o una parte de la frontera que no entro al numero son filas del historico, y ordenadas por nivel caian detras de las medidas; con max_filas por omision, la de una medicion de 120 celdas quedaba afuera y el informe solo decia "se muestran 100 de 121 filas". - El informe nombra los pares medidos fuera del marco: el alcance los contaba -medidos_fuera_del_marco- y print() los nombraba; el informe no decia cuales. - Los caracteres de control se escriben con su codigo en el informe: salian crudos en la moda, los ejemplos y los patrones, y un navegador no los dibuja -ctl\x01x se veia ctlx-, mientras la evidencia del mismo informe los escribia . Ahora todo el informe los escribe asi, y el encabezado lo declara. El plan de limpieza: - conservar_primera_duplicada ya no borra filas que no son duplicadas: con una columna integer64 y otra columna, comparaba con duplicated.data.frame() crudo, que le saca la clase y lee dos negativos distintos como la misma fila; sobre id = -1, -2, 3, -1 borraba tambien la fila del -2. Ahora usa la misma definicion de "misma fila" que el marcador del grupo y que el perfil. - marcar_filas_duplicadas se ejecuta con columnas matriz, data.frame anidadas o POSIXlt -lo que deja strptime()-: el plan la recomendaba y la activaba, y aplicar() la registraba fallida. Las dos acciones aplanan la tabla como el perfil, y un factor se compara por sus codigos (desde la ronda 30 el nivel NA es un faltante, como el codigo ausente, en el perfil y en las acciones de duplicados). Con datos, el plan bloquea las acciones de duplicados cuando hay una lista que no se puede comparar -tambien dentro de una tabla anidada o en una geometria-, que es la pregunta que hace el ejecutor. - winsorizar_outliers recorta a los limites de Tukey y no toca lo demas: cuando los extremos movian los cuartiles, reemplazaba los que quedaban por la mediana y, si seguian, llevaba la columna entera a un solo valor -24 celdas en 52,5 con el plan diciendo 6-. Ahora es un solo recorte, el que la ayuda promete; el perfil del resultado puede volver a ver extremos con los cuartiles nuevos. - reemplazar_separadores cambia solo los codigos 9 a 13 que cuenta el hallazgo: el patron \v de PCRE casa tambien U+0085, U+2028 y U+2029 -que son de las acciones hermanas- y, sobre celdas bytes, el byte 0x85 de una letra como la A con anillo, que quedaba UTF-8 invalido. - La imputacion por dependencia respeta la aplicabilidad: llenaba las filas declaradas fuera del universo -vacio por diseno- y el re-perfil acusaba valor_fuera_de_aplicabilidad. n_afectadas las excluye. - convertir_tipo a entero no pierde -2147483648: el limite inferior era NA_integer_, la conversion lo volvia NA y el plan la publicaba recomendada, activa y con cero valores perdidos. Ahora no es ejecutable, como 2147483648. - Los centinelas de una columna protegida se convierten sin publicarse: el plan tapaba en valores los mismos numeros que la accion necesitaba, y la dejaba lista y sin efecto. El catalogo del paquete se publica entero -es vocabulario, y el hueco delataba cual tenia la columna-; un centinela declarado sigue tapado, valores_enmascarados = TRUE lo dice y aplicar() lo resuelve sobre los datos con los rellenos de la columna. Si eso no reproduce lo que el plan estima, la accion queda bloqueada y dice como ejecutarla. - El plan no nombra marcadores que el detector descarto: leia los datos contra el catalogo completo, publicaba sd y nd en columnas de diez o mas valores distintos y por ellos dejaba sin recomendar la conversion de S/D. Detector y plan comparten la regla. - Planificar de nuevo sobre lo aplicado no actua sobre las marcas del plan: marcaba ausentes en .grupo_duplicado -cuyo NA es "no participa"- y volvia a recomendar marcas que ya existian, que fallaban en cada ronda. Los hallazgos sobre las marcas quedan en hallazgos_sin_accion con su motivo, y una accion cuya marca ya existe queda bloqueada. La ayuda decia que con las marcas incluidas un perfil posterior no vuelve a contar duplicadas: en un grupo de tres o mas filas las repetidas siguen iguales entre si, y ahora lo dice. - Ampliar valores de convertir_ausencias_textuales convierte lo agregado: la lista solo podia acotar -decision escrita en un comentario- y el registro publicaba como aplicada una lista que no se aplico. Ahora la lista manda, como en convertir_sentinelas_numericos. - guiar_limpieza() imprime el numero que resuelve: imprimia el indice dentro del grupo y resolvia la posicion entre las elegibles; con un plan reordenado, elegir el numero que se veia dejaba el grupo omitida. - Quitar filas o convertir el tipo conserva la etiqueta de variable, y quitar filas de un data.table conserva su clave. Ronda 25: la proteccion otra vez, el historico entre sesiones y las claves Tres refutaciones en paralelo. La de la proteccion encontro un aborto -la proteccion por omision hacia abortar perfilar() sobre una tabla con una columna personal y un nombre de columna con tilde-, cinco fugas y seis familias de tapado de mas; la del historico y la deriva entre sesiones, ocho defectos; la de las claves, las relaciones y el referencial, diez. La proteccion: - Un nombre de columna no ASCII ya no hace abortar la proteccion: con una columna personal y un nombre como Anio Ingreso escrito con enie, perfilar(), analizar(), perfilar_por() y perfilar_dbi() abortaban por omision con "no se permite traduccion de cadenas con bytes": la proteccion comparaba los nombres citados entre comillas -la evidencia de los nombres no sintacticos en memoria, la SQL guardada en una base- con una marca de codificacion que R no deja traducir. Ahora compara por bytes, y el nombre citado se conserva entero tambien cuando no es ASCII; antes, citado solo, ni siquiera se apartaba. - clasificar_variables() y detectar_discordancias() aplican el piso de perfilar(): protegian solo por columna, y el documento protegido escrito en otra columna -una observacion que cita la cedula- salia en los niveles observados y en evidencia mientras perfilar(), analizar() y distribucion_valores() lo tapaban sobre la misma tabla. Es el mismo defecto ya arreglado en esas hermanas. - La fecha con hora protegida se tapa escrita con barra, arroba, guion doble o corchete entre la fecha y la hora -2024-06-07 / 09:04:05, 2024-06-07 [09:04:05]- y con la hora compacta tras la fecha con separadores, 2024-06-07T090405: se publicaban, mientras sus hermanas -, ; y | se tapaban desde la ronda 24. - Todo asterisco de Unicode es comodin: solo seis de los cuarenta y siete lo eran, y el documento sin su primer digito tras el asterisco de ocho rayos del emoji se publicaba. Quedan fuera los dos signos cuneiformes, que son letras, la etiqueta invisible y el asterisco combinante de abajo, que el pliegue borra como a una tilde (declarado). - El correo protegido escrito con palabras se tapa: juan(at)gmail.com, [at], {at}, _at_, (arroba), (a) y juan arroba gmail punto com se publicaban -diez de doce formas-. La palabra se lee como arroba solo si la sigue un dominio con su punto, y dot o punto como punto solo en un texto con arroba: medido sobre 3.000 correos protegidos, los no protegidos escritos asi se tapan lo mismo que con la arroba comun, y ninguna de 2.013 frases con esas palabras. - Las letras modificadoras de acento se pliegan como las tildes: el nombre protegido escrito con el agudo modificador U+02CA en lugar de la tilde se publicaba en la moda, los ejemplos y el HTML de reportar(), mientras con el acento U+00B4 se tapaba. El pliegue de comparacion borra ahora U+02C6 a U+02D1. - Una sola regla decide si una columna protegida es de fechas, en memoria y en perfilar_dbi(): la mitad o mas de sus valores distintos con forma de fecha, con el ano en cuatro cifras o en dos, o AAAAMMDD. Eran dos -la columna AAAAMMDD tenia que serlo en todas sus celdas y la otra en la mitad, sin contar el ano en dos cifras-, y una columna de nacimientos con el 60 % en dd/mm/aa, o una AAAAMMDD con una celda mala, tapaba la moda y los ejemplos de las fechas de otra columna (regresion de la ronda 24). La lectura de la tabla entera en perfilar_dbi() no aplicaba ninguna. El relleno de una columna de fechas sigue en el piso. - El relleno de una columna protegida no vacia el perfil de otra: el 99999999 de una cedula entraba al piso y tapaba la moda, el minimo, la mediana y el maximo de un monto con su propio 99999999 -y el hallazgo decia "el valor [valor protegido] aparece 30 veces"-, y el ------ de un telefono tapaba sin observaciones ------ revisar. Fuera del piso: el relleno sin letras ni digitos, y el digito repetido que se repite cinco veces o mas en una columna que sin el es casi una clave. Donde el documento se repite -una tabla de movimientos- el digito repetido sigue en el piso, y tambien el centinela que el usuario declaro en sentinelas_numericos. La columna protegida los sigue tapando por columna, tambien en los parametros del plan. - detectar_discordancias() clasifica la tabla entera solo si la evidencia cita filas: es lo que necesita el piso, y fuera de ese caso cuesta lo mismo que antes. - La IP con mascara CIDR y el importe largo con miles se prueban enteros: 10.4.123.234/24 se tapaba por sus tramos de octetos -uno de cada seis-, y 3.919.024.166 o $ 5.902.468.069 por el tramo de sus tres primeros grupos, con forma de documento sin verificador -uno de cada seis-. Ahora la IP se separa de su mascara y el importe de cuatro grupos o mas se compara entero, como la IP; los dos quedan en cero en data-raw/medir_tapado_de_mas.R. - Documentado el costo de nombres y apellidos protegidos por separado: un departamento, una calle o un liceo que comparte palabra con uno de ellos se tapa -trece campos de cuatro columnas sobre 5.000 personas-. - Declarado con su cifra lo que se tapa de mas por decision: la hora con fraccion de ocho cifras y el numero con forma de ISBN-10 con su control invalido -cuatro o cinco de cada cien-, y lo que se une como el documento: la lista con espacios, el rango de anios, la lista con tabuladores o con comas sin espacio -tambien la coma ideografica- y dos numeros en dos lineas. Todas las familias nuevas estan en data-raw/medir_tapado_de_mas.R. El historico y la deriva: - Un historico exportado con write.csv() conserva la configuracion de sus corridas: la tabla, el marco, los tipos, el modelo, la aplicabilidad y el perfil vivian solo en el atributo configuracion_evaluacion, que el CSV no escribe, y la deriva del historico releido agrupaba todo bajo : restaba la corrida de una tabla a la de otra (delta -1, error) y publicaba como deterioro un cambio de marco que el objeto declaraba no comparable. Ahora viajan tambien en las filas evaluacion_perfil, en seis columnas nuevas (identidad_tabla y configuracion_*), y el historico releido las recupera de ahi. El esquema sigue en 1: las columnas son opcionales a la entrada y un historico anterior se lee igual. Y un par al que le falta la configuracion -un CSV anterior, una tabla armada a mano- no recibe veredicto: cambio = "no_comparable" y cobertura_diagnosticos lo declara. - Acumular sobre un historico releido de un CSV es idempotente: read.csv() lee como logica una columna de texto que viene entera en NA, y re-acumular la corrida que ya estaba se rechazaba con "Difieren: id_medida (NA contra NA)". Cada columna recupera el tipo del esquema al validarse. - Una medida suprimida no publica su valor en la fila de otro perfil: la supresion ya registrada tapaba solo el nivel medida, y la fila evaluacion_medida de la misma medida evaluada por OTRO perfil publicaba su numero -con x > 0 sobre una metrica booleana, el valor suprimido mismo-, en el historico y en su informe. Con las dos reglas en un solo perfil se tapaba. Ahora la supresion vale en los dos niveles, para cualquier perfil o regla y en cualquier orden de llegada. - La deriva lee la frontera de cada corrida: una coleccion que midio sus tres tablas y despues dos publicaba la diferencia como mejora -y como deterioro error al volver la tercera-, con los datos de cada tabla identicos. El historico guardaba la parte no medida desde la ronda 24, pero la deriva no la consultaba. Ahora un par cuyas partes sin medir difieren no publica la fila de resultado y la declara con aspecto = "cobertura_frontera" y cambio = "no_comparable", como el cambio de marco. - rbind() de dos corridas conserva la parte no medida de cada una en el historico: historico_calidad(rbind(ene, feb)) no guardaba ninguna parte_no_medida donde historico_calidad(ene, feb) guardaba las dos, y la coleccion incompleta quedaba registrada como completa. Unidas dos corridas distintas, cada cobertura de frontera lleva su id_medicion, y la parte va a la corrida a la que le falto; una cobertura que no se puede atribuir se rechaza en vez de callarse. - comparar_evaluaciones() toma el delta de la deriva: restaba por su cuenta y publicaba -0,75 entre dos tablas distintas medidas con el mismo perfil, o entre un marco y otro, donde la deriva del historico de las mismas dos evaluaciones no arma par o declara no_comparable. Era la misma regla escrita dos veces, como la que reportar() ya habia corregido. Ahora el delta es el de detectar_deriva_calidad() y una columna nueva, comparacion, dice por que falta. - Una fecha de texto con desplazamiento se lee con el: 10:00:00-03:00, T10:00:00-0300 y 10:00:00 -03 se leian como las 10:00 UTC -son las 13:00-, y 10:00:00 basura pasaba igual, porque as.POSIXct(format = ) ignora lo que sigue al formato. En el cambio de hora eso invertia el orden de dos corridas de un historico exportado desde una base y el veredicto de la deriva. Ahora el desplazamiento se aplica (Z, UTC, +HH, +HHMM, +HH:MM) y un texto que no se lee entero se rechaza. Vale para el historico, medir(), perfilar() y reportar(), que leen la fecha por el mismo camino. - Un cambio de modelo que mantiene la comparacion no se etiqueta no_comparable: la deriva publicaba para el mismo par la fila de resultado con veredicto (-0,75, error) y una fila configuracion_modelo con cambio = "no_comparable" y la descripcion "se mantienen las comparaciones"; y el informe, que confiaba en la etiqueta, borraba el delta. Ahora no_comparable queda para el cambio de marco o de tipos -cuando la fila de resultado se quita-, y la evolucion del informe y comparar_evaluaciones() leen las filas de cada par con una sola regla: el delta de la deriva y, al lado, lo que la deriva declara. - La ayuda dice dos limites que el paquete ya tenia y no declaraba: suprimir tapa el valor de la medida, no lo que se deduce de otras publicadas -un agregado suprimido se recalcula de sus medidas crudas si el historico las lleva-; y la deriva compara corridas consecutivas, asi que una corrida sin evaluar en medio de la serie no se salta: para comparar sus vecinas, se quita del historico. Claves, relaciones y el referencial: - Un texto con bytes invalidos se compara por sus bytes, no como ausente: lo que deja un read.csv() de un archivo latin1 sin declarar se volvia NA al comparar, y todos los invalidos casaban entre si. detectar_relaciones() publicaba coberturas 1 y 1 sobre dos tablas con una sola fila en comun, el referencial declaraba conforme a una persona que no estaba en el padron (RatioCobertura 0,5 donde era 0) y detectar_dependencias() juntaba los invalidos en un grupo y descartaba la columna como casi constante. Ahora la comparacion usa la clave de bytes del paquete, como ya lo hacia la unicidad normalizada de detectar_claves(); la proximidad del referencial no compara esos valores -una distancia de texto no se mide sobre bytes que no son texto- y lo declara en su motivo, y distribucion_valores() los sigue descartando y declarando el alcance parcial. La evidencia de detectar_dependencias() los escribe como la consola -"Mar\xeda"- y no como NA. - relaciones_coleccion() no compara un entero ancho que llego como doble: dbConnect(duckdb::duckdb()) entrega BIGINT como doble, y por encima de 2^53 dos identificadores distintos caen en el mismo doble. Tres valores contra otros tres, ninguno comun en el motor, salian m:1 con 2 comunes y cobertura 1. Una columna leida como doble con algun valor de al menos 2^53 deja ahora sus pares sin_comparar, con motivo entero_como_doble en cobertura_podas y el remedio -conectar pidiendo integer64- en el detalle, como ya hacia perfilar_dbi(). Mira la columna y no solo la conexion: HUGEINT y DECIMAL llegan como doble aun pidiendo integer64. - Un integer64 negativo es un entero, no un ausente: bit64 no registra anyDuplicated() y complete.cases() no despacha, asi que los dos leian los bits como doble, y todo entero de -1 a -4503599627370495 tiene el patron de un NaN. detectar_claves() no proponia id = -1, -2, -3 (y sugerir_clave() si), referencial() lo rechazaba por "valores ausentes", la correctitud referencial borraba en silencio la fila que fallaba (RatioCobertura 0,333 donde era 0,667), y perfilar() publicaba filas_completas = 0, filas distintas como duplicadas y dos columnas distintas como identicas. Ahora esas comparaciones usan el texto exacto del integer64. - Instantes y fechas con fraccion se comparan por su valor: relaciones, referencial y dependencias escribian un instante al segundo y una fecha al dia, mientras la poda por rangos usaba el numero entero, asi que la misma tabla daba dos respuestas segun sus vecinos (sin_coincidencias por la poda, o m:1 con cobertura 1 sobre un valor que no estaba), el referencial declaraba conforme un instante ausente del padron y detectar_dependencias() no omitia una columna que era clave. La escritura conserva ahora la fraccion exacta; sin fraccion es la de siempre, y un texto al segundo sigue encontrando su instante. - detectar_claves() no cuenta los faltantes disfrazados como colisiones: el comentario decia cerrado el caso y seguia vivo en la funcion exportada. 190 documentos distintos mas diez "SIN DATO" salian casi-clave con concentracion 1, mientras perfilar() contaba diez faltantes disfrazados y ningun hallazgo; y una colision real (6 excedentes, concentracion 1) se publicaba con 19 excedentes y concentracion 0,684. La mascara se calcula ahora en una sola funcion que usan perfilar() y detectar_claves(), con la politica del perfil cuando se lo pasa. - RatioCobertura sobre una entidad sin valores queda sin_valores: sin filas, o con la clave toda ausente, publicaba 0, que el tablero lee como un fallo; la ayuda ya decia sin_valores, como la correctitud. - El universo de RatioCobertura se cuenta con la identidad del apareo: un padron A, a, B -unico por identidad exacta- y una entidad con a daban 0,667, que no es ni la cobertura normalizada (0,5) ni la exacta (0,333). Dos claves que la normalizacion funde son una sola clave del universo, y n_referencial cuenta las de ese universo. - podar = TRUE ya no declara imposible una cobertura alcanzable: la cota de "cardinalidades imposibles" era de valores distintos y la cobertura es por filas; un solo valor comun en 91 de 100 filas se declaraba imposible con umbral 0,9 y la FK se perdia. La cota es ahora la de los valores mas frecuentes. - relaciones_coleccion() no poda con un rango de tipos mezclados: en SQLite un MAX sobre una columna sin afinidad puede ser un texto -todo TEXT ordena despues de todo numero-, y la poda "cierta" por rangos disjuntos declaraba sin coincidencias 11 filas de 11. Un extremo que no llega como numero deja la columna sin rango y la comparacion se hace. - Un par con la tabla de referencia truncada no se descarta en silencio: la cobertura medida contra una referencia leida a medias es una cota inferior, y una FK perfecta de 20.000 filas leidas de a 10.000 daba 0,496 y desaparecia de todas las salidas. Ahora queda en cobertura_pares como sin conclusion, con la mayor cobertura medida y como resolverlo, y meta$pares_sin_conclusion los cuenta. - La clave de un texto ASCII con barra invertida no depende de sus vecinos: a\b tenia una clave en un vector todo ASCII y otra -con la barra duplicada- en uno con algun acento, asi que detectar_relaciones() no lo encontraba entre dos tablas cuando solo una tenia una letra acentuada. Ronda 24: los niveles institucionales, las cifras del perfil y la proteccion otra vez Tres refutaciones en paralelo: la de los niveles institucionales encontro once defectos; la del perfil contra conteos a mano, otros once -uno lo defendia una entrada anterior de este archivo, que se corrige-; y la de la proteccion, nueve fugas y siete familias de tapado de mas. Los niveles institucionales (agregar() de la coleccion para arriba): - La cobertura de cada parte sobrevive a rbind(): dos colecciones agregadas por separado y unidas dejaban solo la cobertura de la primera, asi que el conjunto -o la organizacion- salia completo o incompleto segun el orden de la union, y la tabla que no habia entrado desaparecia. Ahora cada parte conserva la suya en cobertura_de_partes, partes_incompletas nombra la parte y no el atributo, y el tablero, el indice y el informe reunen todas las colecciones. - Una tabla y una columna con punto ya no se funden: ventas.total con mes y ventas con total.mes caian en un solo atributo (0,625 donde habia 1 y 0,25), porque el grupo se armaba pegando los nombres con un punto. Los identificadores escapan sus separadores, y desde la coleccion para arriba nombran las partes que reunen: dos conjuntos distintos, o dos colecciones sin nombre, ya no comparten id_medida; un nombre con coma va entre comillas invertidas en la parte unida. - El alcance suma las partes completas aunque se hayan agregado por separado: una coleccion completa unida a una parcial entraba como "1 de 1" y el conjunto publicaba "5 de 9" donde eran 12 de 16. - Una regla sobre un agregado no da por completa una parte sin medir: la regla nombra agregada:ratio:NoNulo y la cobertura NoNulo@a2.x, y no casaban; el numero que excluia la parte se evaluaba como completo y la deriva lo leia como mejora. Y una coleccion lleva en cobertura_metricas solo la de sus tablas: la de otra coleccion le cambiaba el veredicto. - La evaluacion y el historico nombran la parte que no entro: evaluar() conserva la cobertura de la frontera, la imprime y la lleva a su informe, e historico_calidad() la guarda como nivel parte_no_medida. - El nombre de lista manda en los tres niveles: organizacion y conjuntoColecciones abortaban cuando la lista renombraba sus colecciones; y los pesos se aceptan con el nombre declarado o con el del objeto. - Los pesos que casan con dos vocabularios y reparten distinto se rechazan (antes ganaba la tabla en silencio); el mensaje de pesos de agregar() dice cuando un nombre difiere solo en su forma Unicode, como el de indice_calidad(); y agregar partes de dos corridas se rechaza diciendo por que -un agregado describe un momento- y que hacer. ?organizacion prometia reunir colecciones de momentos distintos: ahora dice que la organizacion las declara y que para agregar se miden en una corrida. - Las claves del tablero y de las dependencias funcionales tampoco pegan nombres con un punto -la misma falla, en ramas que la refutacion no alcanzo-: llevan el separador de control que las evaluaciones usan desde la ronda 22. Lo que afirma el perfil: - Una columna data.frame anidada es compuesta, como una matriz: la que deja jsonlite::fromJSON() o un tibble empaquetado caia por el camino comun e is.na() contaba celdas. Doce filas con 27 celdas ausentes publicaban n_faltantes = 27, prop_faltantes = 2.25 y una traza hasta la fila 34. Ahora publica n de filas, los estadisticos por valor en NA y el hallazgo tipo_compuesto_no_analizado, con todas sus filas en la traza. - Los centinelas declarados no corren las trazas: con sentinelas_numericos, las trazas de valores_no_finitos, ceros_no_permitidos, negativos_no_permitidos y outliers se calculaban sobre el vector sin centinelas y cada fila quedaba corrida tantas posiciones como centinelas hubiera antes: el cero de la fila 20 se trazaba en la 17, con estado disponible. Pasaba en perfilar(), en perfilar_dbi() y en perfilar_por(), y tambien en integer64. - La traza de posible_centinela_numerico convierte como el resumen: en una columna factor o en texto con coma decimal se buscaba el centinela con as.numeric() sobre la columna cruda -codigos de nivel, o NA-, y la traza salia vacia con el aviso de incoherencia. Una traza que no encuentra filas publica total = NA y no cero. - El NaN no se descuenta dos veces del denominador de ceros_no_permitidos, negativos_no_permitidos, outliers y posible_centinela_numerico: se derivaba restando n_faltantes y n_valores_excluidos_resumen, y el NaN esta en los dos. c(0, 1, 2, NaN, NaN, NaN, 5, -3) publicaba n_evaluados = 2 donde la media promedia cinco valores, y la misma columna con NA publicaba 5. Ahora se lee el conteo hecho donde se calcula el resumen, como ya hacia la fila de cobertura. - La traza truncada de mayusculas_inconsistentes y normalizacion_unicode trae primero las variantes, como ya hacia casi_duplicados_vocabulario: treinta "Montevideo" y un "MONTEVIDEO" en la fila 31, con max_filas_hallazgo = 10, publicaban diez filas identicas y ningun testigo de la colision; con dos grupos, el segundo quedaba entero afuera. - umbral_patron_raro es inclusivo, como dice la ayuda -"maxima frecuencia"; excluidos los que lo "superan"-: un patron en exactamente el umbral -cinco filas de cien con el 0,05 por omision- quedaba afuera contado como si lo superara. La frontera estaba escrita en tres lugares, la via por bloques incluida, y cambia en los tres. - perfilar_por() traduce a la tabla original tambien indices_unidades -la primera fila de cada forma en casi_duplicados_vocabulario-, no solo indices_fila: con grupos intercalados nombraba filas de otro grupo. - patron_raro medido sobre una muestra sin hallazgo deja fila en cobertura_diagnosticos: con muestra menor que la tabla, seis filas raras fuera de la rejilla publicaban cero hallazgos y una cobertura vacia -un perfil "limpio" para quien decide por nrow(cobertura_diagnosticos)-. - patron_raro publica como n_evaluados el universo de sus proporciones -las filas con valor-, no las filas analizadas con sus NA: cien filas con dos NA publicaban n_evaluados = 100 junto a proporcion_dominante = 0.918, que es 90/98, y 1 - n_afectados/n_evaluados no reconstruia la proporcion publicada. descubrir_patrones() publica ese denominador en el atributo n_evaluados. Correccion de una entrada anterior: en "Conteos que describen lo que miraron", la vineta de posible_centinela_numerico decia que "los diagnosticos que miran la columna entera -patron_raro, faltantes, filas_duplicadas- siguen en 28, que es su universo real". Para patron_raro era falso: una fila NA no tiene patron, sus proporciones se calculan sobre las filas con valor y su traza no la mira. En ese ejemplo su universo es 27; faltantes y filas_duplicadas si miran la columna entera y siguen en 28. - Con universo = "muestra_motor" y un orden no demostrado estable, la traza retenida se publica no_disponible con alcance = "orden_muestra_no_estable" -sin indices, con su total- y no NULL, fuera de los cuatro estados; y n_patrones_raros_trazabilidad conserva la cantidad de patrones raros en lugar de forzarse a cero. - La moda empatada de resumen_tabla por bloque_filas sigue el desempate del resto del paquete -orden numerico para numeros, bytes para texto-: tomaba el primero del mapa, y la misma tabla publicaba 3.25 en memoria y por SQL y 10.608 por bloques. La proteccion: - perfilar_dbi() sin muestreo -la llamada por omision- relee como texto, en SQLite, la columna protegida que el controlador leyo como numero: un documento guardado como texto en una columna de afinidad numerica llegaba como 5 y se publicaba en la moda y los ejemplos de otra columna. Con muestreo ya se tapaba. - Un valor protegido que contiene un nombre de columna -el correo juan_perez@empresa.com.uy con una columna juan_perez- se busca entero antes de apartar el nombre: con otra caja (JUAN_PEREZ@EMPRESA.COM.UY) ya no se publica, y la aguja recortada de la ronda 23 (@empresa.com.uy) ya no tapa el dominio ni el correo de otra columna. - Cada marca o nombre apartado lleva su propio codigo y se repone en su lugar: si la proteccion tapaba un tramo que llevaba uno, los siguientes se corrian y el texto publicado nombraba otra columna. - Un telefono guardado con forma de fecha (2012-11-05) en una columna protegida que no es de fechas vuelve al piso: la ronda 23 estrecho el rango de anos, pero el piso seguia decidiendo por el valor. Ahora decide por la columna -es de fechas si es Date/POSIXt o si la mitad de sus valores distintos tienen forma de fecha-, en perfilar(), analizar(), el plan, perfilar_por(), los duplicados aproximados, perfilar_dbi() con y sin muestreo y las demas puertas que cosechan los datos. - La regla de digitos reconoce mas escrituras del mismo documento: la raya, la raya larga y el apostrofo de un CSV de Windows leido como latin1 (U+0096, U+0097, U+0092); los invisibles que no son de formato -marcas combinantes y envolventes, ignorables, controles- y mas parecidos del apostrofo (el acento agudo del teclado en espanol), del punto (el ano teleia, la vineta), del guion, de la coma y de la barra; y los asteriscos de Unicode como comodin, tambien el asterisco pegado a una letra (CI*123.456-7). - La fecha con hora protegida se reconoce con dos espacios, -, ;, _, | o parentesis entre fecha y hora, y con la hora separada por guiones o escrita 21h24m25s. La fraccion de segundo tras un punto ya no se traga un documento pegado: de una a siete cifras o nueve, nunca ocho, y no seguida de un guion y una cifra. - Un ISBN-10 se borra solo con su digito de control valido o la etiqueta ISBN delante: con cualquier diez cifras en cuatro grupos, un NIT 900-123-456-7 se borraba y se publicaba. Y se reconoce tambien con espacios o puntos. - Tapa menos de mas: una direccion IP solo se tapa cuando coincide entera -antes tambien por un tramo de sus octetos-; un ISBN-10 con espacios o puntos, una fecha con espacios y el ano al final (16 01 2002), y la equis seguida de un espacio (92 x 6931234, pack x 1781643) ya no se toman por documentos. Medido con el reproductor: la IP de 2,0 % a 0 % (y al final de una frase de 1,5 % a 0,8 %, las que coinciden enteras), el ISBN-10 con espacios de 3,2 % a 0 %, con puntos de 0,8 % a 0 %, la fecha con espacios de 7,2 % a 0 %, y 52 x 6037844 de 29,5 % a 18,8 % -lo que queda es el costo declarado de todo numero de siete cifras-. La equis pegada al numero (serie X6276963, 27,3 %) y la coordenada con cero adelante (-056.092164, 0,8 % a 2,2 %) se siguen tapando: la primera es como se escribe el documento recortado sin formato y la segunda tiene la forma de un celular con punto (099.123456). Quedan declaradas en ?perfilar con su cifra, y el reproductor data-raw/medir_tapado_de_mas.R las mide con ocho familias nuevas. - ?perfilar declara lo que queda: una fecha con hora cuya fraccion de nueve cifras tras un punto empieza con un cero que es parte del documento, un documento de diez cifras escrito en cuatro grupos que por azar valida como ISBN-10 (uno de cada once), el telefono con forma de fecha escrito en otra forma que la guardada, y los parecidos raros (la prima de una coordenada sexagesimal, el acento grave, la tilde). 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 conserva alcance_medidas. - ratio_umbral alcanza el umbral en el borde -1 - 0.9 llega a 0.1-, con la misma cuenta en el tablero y en agregar(); 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 mismo id_medicion y unidas con rbind()- salian con los mismos id_medida: evaluar() y historico_calidad() las rechazaban como una medicion unida consigo misma, en el camino documentado para subir de nivel. Ahora medir() antepone la metrica instanciada y numera dentro de ella (M-NoNulo@t.x-000001), y agregar() 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; un umbral que 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. ?perfilar lista 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() y rbind() 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 de rbind(), 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. - AtributoDuplicado marca todas las apariciones tambien en integer64, y los conjuntos declarados -valores_nulos, valores, diccionario- y la regla de integridad entre entidades emparejan los numeros por su valor: 1e5 y el integer64 100000 coinciden. - ErrorEstandar se declara no medible ante un valor infinito, como Escala, en vez de publicar la dispersion del resto. - evaluar() mira la cobertura de las reglas corrida por corrida, rechaza una medida repetida o con resultado NA nombrandola, 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 con write.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 con rbind(), 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.89 o CI 4.123.456.7 se 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-12 no 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_nombre salia [valor protegido]_nombre en los hallazgos sin accion del plan, en columnas_analizadas y en la SQL guardada de perfilar_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() y rbind() -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 titulo ya no se escribe como
dentro de . 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.R rehace 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 maximo de 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 que perfilar(). 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_titulo pone la mayuscula en la inicial aunque este acentuada -Pena con enie salia PenA- y capitaliza fuera del latin; las tres acciones de caja usan el mapa del paquete y no dependen del locale. - winsorizar_outliers conserva la clase de la columna: sobre texto con numeros la pasaba entera a numero y borraba los ceros iniciales que el propio plan protege; Inf y NaN quedan 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 valores vacia en convertir_ausencias_textuales no 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.2 no es 0.3, latin1 no es UTF-8, y conservar_mas_completa no junta las claves ausentes ni borra una fila con clave distinta. - normalizar_nombres aplica los nombres_propuestos editados y no renombra las marcas que agrega el plan; sobre un sf actualiza su geometria. - El plan no tapa el catalogo de centinelas ni el vocabulario compartido de columnas no personales, y con proteger_datos_personales = FALSE no 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; un orden de texto se rechaza; activar a mano una reparacion de codificacion parcial la aplica; una accion recomendada ejecutada no figura pendiente; 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 la unidad de 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() declara no_comparable una 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>Perez en 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 en perfil_muestra$datos, que no existe: $ hacia coincidencia parcial con datos_personales y 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, Blanco tapaba toda evidencia con la marca <blanco>, Maximo la que llevaba la clave grupo_maximo, y Patron o Constante descripciones 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 con aplicable. Se publicaban, entraban en el agregado y alcance_medidas decia «midio 4 de 5 en el universo aplicable» cuando adentro se habian medido 3. Es una salida que no cumple el contrato de metodo, como una fila que no existe: la metrica queda no_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 publicaba sin_valores con «aportar valores no nulos», que ?medir define 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 >= 66 aunque pila y edad estuvieran 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 en etiquetas_personales con tipo contiene_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.uy frente 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.perez se 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ÑO se normalizaba a ano y año a año: CAÑADA no encontraba a Cañada en el referencial, y PEÑA se juntaba con Pena en vez de con Peñ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 bajo C, y detectar_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 FALSE sin declarar nada. Ahora quedan en NA, con su fila en la cobertura, igual que cuando falta bit64. - 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_disfrazados dejaba 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 que perfilar() 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.2 y 0.3 coincidian, y CorrectitudSemFuerte declaraba conforme un identificador que el padron no tenia; y el mismo numero guardado como entero en una tabla y como doble en la otra -100000L y 1e5- no se encontraba. EntidadDuplicada tenia 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 y detectar_dependencias() publicaba k -> k. Ahora no se analizan y se declaran: una fila sin cifras en sugerir_clave() (y elegir_clave() no la ofrece), un aviso en detectar_claves(), el par sin_comparar con motivo_poda = "nombre_repetido" en detectar_relaciones(), y el motivo nombre_repetido en las dependencias, que perfilar() declara en su cobertura. - detectar_relaciones() no compara una fecha con una fecha-hora. Las dos escrituras no coinciden nunca, y la fila decia sin_coincidencias con cobertura 0 sobre los mismos dias. Sale sin_comparar, con motivo_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 de scipen (1e+07 o 10000000) y de OutDec (0,5). La configuracion se guardaba asi: la misma corrida medida con otro scipen no 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 de perfilar_por() y el emparejamiento del referencial tampoco dependen de esas opciones. - guardar_historico(), guardar_analisis() y reportar() 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. - aplicabilidad sobre una columna de geometria. perfilar() y analizar() abortaban si la regla dejaba afuera la primera fila de una sfc, 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_identificador y patron_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 en descubrir_patrones(). - medicion_desde_estimaciones() produce una medicion que evaluar() acepta. Declaraba real -una proporcion en [0, 1]- para la estimacion, el error estandar, el coeficiente de variacion y los demas, y evaluar() rechazaba entera cualquier medicion con una estimacion mayor que 1. Ahora son numero_real. Ademas lee un factor por el texto de sus niveles -antes publicaba sus codigos como estimaciones- y descarta, con aviso y en attr(, "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. Con max_ejemplos = 0 decia "sin filas discordantes" al lado de n_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 a Inf: una columna sin valores finitos queda desconocida. Y n_niveles_observados es NA, 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é" daba Aa+é-. 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, un 99991231 quedaba fuera del formato -el rango admitido es 1800 a 2100- pero el perfil lo publicaba como maximo_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 de strptime() 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/2020 quedaba 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 (12000000 era una cedula invalida, y con scipen lo era cualquiera); las mayusculas son solo ASCII (toupper() volvia ſe un codigo ISO); una URL marcada latin1 vale lo mismo que en UTF-8, y NaN es ausente. - perfilar_por() etiqueta cada grupo con su valor. Un integer64 por 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 en cobertura_grupos, para que la reconciliacion de filas cierre; un nivel (ausente) declarado y vacio se declara aunque haya NA; un factor con NA como nivel ya no aborta. Los nombres de columna repetidos y un min_filas fraccionario 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 decia equivalente con 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 publicaba n_codificacion_no_se_pudo = 1 sin hallazgo ni accion. - La deriva declara un cambio de cadenas_ausencia y 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 constante decia «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. - ?perfilar declara 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_medicion y una fecha. Dos corridas con el mismo id y fechas distintas se mezclaban en agregar() y en el tablero sin aviso. - Un NaN del motor no se publica como una cifra calculada. duckdb conserva NaN como valor y respondia MAX y AVG con NaN: salian en NA con estado calculado. Ahora las metricas de magnitud de esa columna quedan no_disponible con el motivo verdadero -antes culpaba a la magnitud de la columna-, y la mediana tambien: el motor ordenaba el NaN como el mayor valor. - La sonda de magnitud usa MAX, que ve un NaN en duckdb y PostgreSQL y un texto colado en una columna numerica de SQLite; con MIN no veia ninguno de los dos. - ?perfilar_dbi declara lo que el motor guarda distinto que R: las fechas en SQLite -con el remedio, extended_types = TRUE-, el NaN en duckdb y la media en coma flotante sobre magnitudes muy distintas. Ronda 10: lo que publica un metodo propio - Una fila que no existe deja la metrica no_medible. Sin recorte por aplicabilidad no habia tope: fila = 99 sobre cuatro filas se publicaba como medida y viajaba al historico, y una posicion mayor que 2^31 salia NA despues de la validacion. - La etiqueta objeto de un metodo propio no publica un valor personal. Con la proteccion activa y columnas personales en las tablas que recibe medir(), 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 recibio medir()- 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 metodo deja la metrica no_medible, igual que un metodo que aborta, en lugar de abortar medir() 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+v con una columna a+b se rechazaba siendo legitimo. - fila tiene que ser una posicion entera. Un factor publicaba sus codigos y un texto salia NA, la clausula que la validacion decia hacer cumplir. - Con aplicabilidad, la fila de 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_medible pasa 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, como coeficientes, 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_reversibles se recuenta sobre el resultado restaurado: contaba perdidas que la restauracion del universo ya habia deshecho. - eliminar_filas_ausentes funciona 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. Sin conservar_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 con conservar_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 en cobertura_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_fila de una traza que coincidian con un documento quedaban en NA y lupa se acusaba a si misma -«total de traza no coincide con sus indices»-; y en perfilar_dbi() una columna id = 1..n hacia que el conteo n saliera NA en 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() y analizar(). 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 metodo se 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 sin fila en una metrica por celda o por fila. Medido: un metodo que devolvia dos filas por celda hacia que agregar(, "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, y CorrectitudSemDebil publica dni+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 en cobertura_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- y analizar() 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_medidas cuenta el universo de la metrica. Con la propiedad aplicable de NoNulo, 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 con identical() 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_medicion ya 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 propiedad metodo de EntidadContradictoria al 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. ?medir prometia que una metrica que no puede medirse deja su motivo en cobertura_metricas, y cinco metodos del catalogo llamaban stop(): ErrorEstandar sobre una columna sin dos valores, Escala con valores no finitos, las dos OportunidadAtributoPor* 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 en cobertura_metricas con el estado nuevo no_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 de medir()- 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_nacimiento salio publicada como fecha_[valor protegido]. Las dos guardas que comparan los nombres del perfil con los de los datos abortaban planificar_limpieza() y analizar(), 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 columna documento, 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 - excluidos y se rompio tres veces por eso: el NaN esta en dos de los tres sumandos y se restaba dos veces; el arreglo de eso -descontar n_nan una vez- supuso que todo NaN esta en n_faltantes, y eso es falso para un "NaN" de TEXTO, porque is.na("NaN") es FALSE y la columna lo cuenta como presente. Medido: c("10","20","NaN","30","40") publicaba «sobre 5» con media = 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 el NaN de texto, donde la misma fila publica n_faltantes = 0. Quien cuenta la presencia es n_faltantes, que esta en la misma fila. Los infinitos si declaran presencia, porque un Inf siempre esta presente. - unidad se 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 solo no_aplica: una matriz con attr(m, "units") <- "kg" publicaba unidad = "kg" junto a media = 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.5 donde 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 origen instancia*, seis pesos con dos nombres. Rehacer el numero por el nombre publicado daba 0,9 contra el 0,8 publicado, y partes_con_peso_cero nombraba una entidad que si habia aportado con sus otras filas. Ahora la etiqueta es objeto_medible, unica por fila. - Dos declaraciones distintas ya no colapsan al mismo identificador. La tabla literal m1.d -declarada con esquema = NA, la forma documentada para nombres con puntos- y la tabla d del esquema m1 producen 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$estable sale de la bitacora y no de si el argumento vino. Con orden = list(t11 = "id") sobre dos tablas, la lectura de t09 salia sin ORDER BY -la bitacora lo declaraba- y el objeto publicaba estable = TRUE; con una lista sin nombres, ninguna tabla recibia orden y estable seguia en TRUE. Ahora hay cuatro formas de nota_orden, y la parcial nombra las tablas que quedaron sin orden. - Y sin_valores se lee contra el universo que el resumen mide. Un refutador denuncio que el estado contradice n - n_faltantes con 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 es n_aplicables - n_faltantes, y los valores de afuera del universo se declaran en n_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 NaN se contaba con signos opuestos en dos canales de la misma fila. Una columna de treinta NaN publica n_faltantes = 30 y n_distintos = 0 -el paquete cuenta el NaN como 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 que n_valores_excluidos_resumen cuenta los no finitos- y lo que se corrige es la afirmacion de presencia: el desglose separa ahora los infinitos «presentes y no utilizables» de los NaN «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 - excluidos y el NaN esta en los dos ultimos, asi que se restaba dos veces. Con dos NaN publicaba 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 -m contra km- el mismo numero significa otra cosa y eso sigue siendo error; con NA en 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 sale sospechoso con su propia redaccion, en las dos direcciones. - unidad se publicaba sobre columnas que no publican ninguna cifra. El atributo units lo lleva cualquier clase: un factor con attr(x, "units") <- "kg" publicaba unidad = "kg" junto a media = 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 es estado_resumen_cuantitativo == "no_aplica" y no una lista de clases: una columna units de puros NA si declara su unidad, porque su clase se resume. - difftime quedaba mudo en tres diagnosticos. Con las MISMAS cifras, la columna doble publicaba n_faltantes_disfrazados_numericos = 2 y la difftime publicaba 0, las dos con la misma media distorsionada; y difftime tampoco recibia fila de cobertura en ley_benford ni en relacion_aritmetica_columnas, mientras units la recibe en las dos. Una sola causa: is.numeric() responde FALSE sobre un difftime, 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 que difftime publica media como cualquier numero: ahi su silencio dejo de ser legible. El criterio de la declaracion pasa a ser si la fila publica cifras cuantitativas; Date y POSIXt siguen afuera a proposito, porque su resumen sale en media_fecha y no como numero pelado. - Una fila podia identificar una tabla y traer los datos de otra. El adaptador de colecciones de otra generacion recalculaba identificador y referencia solo cuando FALTABAN, asi que una copia guardada que ya no corresponde a su nombre se usaba tal cual: con una referencia que apunta a t2, el perfil publicaba tabla = t1 con n_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 - difftime publica sus estadisticos en la unidad que declara. Era el unico que se abstenia -todo en NA y no_aplica- y su motivo escrito era que sin unidad publicada no se publica el numero. Con el campo unidad ese motivo dejo de valer, y Date y POSIXt publicaban desvio en segundos desde antes: seguir absteniendose era la inconsistencia. Una columna de minutos publica media = 75 con unidad = "mins", en la unidad DECLARADA y sin convertir a segundos, porque convertirlos publicaria un numero que no esta en la columna. - estado_resumen_cuantitativo distingue dos afirmaciones que compartian un nombre. sin_valores era cierto de una columna vacia y de una de puros NA, y FALSO de una de treinta Inf -que publica n_faltantes = 0 y n_distintos = 1- y de una donde los centinelas se llevaron todos los valores: ahi habia valores y ninguno servia. Medido: cinco situaciones distintas publicaban sin_valores y en tres el nombre mentia. Ahora esas tres publican sin_valores_utilizables, y la presencia se define igual que en n_faltantes -que cuenta el NaN como ausente-, para que dos campos de la misma fila no cuenten cosas distintas. - normalizar_espacios_invisibles se trata como su hermana. Las dos estan en la misma rama del planificador y se trataban al reves: eliminar_controles_invisibles se 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 activa recortar_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. reversible sigue en FALSE en 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 contando n_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 todas media = 2 y desvio = 1, y un Period de dos dias publicaba 172800 sin decir que eran segundos. El campo contesta una sola pregunta -en que unidad estan las cifras que la fila publica- y queda en NA donde no hay ninguna: no inventa. difftime sigue absteniendose del resumen y su unidad viaja igual, porque abstenerse no es motivo para callar lo que la columna declara. Para Date y POSIXt publica segundos, que es la unidad de desvio y 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 de m a km con los mismos valores sale con severidad error, 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 objeto units vive en un symbolic_units cuyo NUMERADOR es un vector: leerlo con [[1L]] publicaba km para una columna en km/h -no una unidad ausente, una equivocada, con la misma forma que la correcta- y con m^2 -que guarda c("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 objeto units de verdad -todos declaraban attr(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 reenviaba muestra a detectar_relaciones(), asi que con muestra > 1e5 valia el tope por omision de esa funcion: el objeto publicaba filas_leidas_1 = 200000 y meta$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 a cobertura = 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 = FALSE mientras el segundo habia leido 10.000 filas de 20.000. Ahora cada fila publica filas_totales_* y muestreado_*, y para saberlo se pide UNA fila mas que el tope -mucho mas barato que un COUNT(*) por tabla, y exacto-: si vuelve, la lectura esta truncada y el total va NA; 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 catalogo y el recorrido encontro SEIS columnas en el mismo estado: sin tipo, esquema, identificador o declaracion la corrida entera moria con un error de data.frame() -«los argumentos implican un numero diferente de filas»- que no nombraba ni la columna ni la tabla, y sin referencia perfilaba 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) y metrica es 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 publican metrica_instanciada y entidad. La instancia sola no alcanzaba: sobre un agregado vale agregada:ratio:<metrica> para todas las filas, y ahi la entidad es lo unico que separa. Y para quien lee, con varias entidades objeto nombra 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_universos se calcula, ya no es una frase fija. Afirmaba «los componentes salen de universos distintos» en toda corrida, incluso con los tres componentes publicando universo = "celdas" -una afirmacion que el propio objeto desmentia en su columna universo- 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 faltaba fecha_declarada. .exigir_orden_temporal() empieza con if (!isTRUE(declaradas)) return(), asi que sin el atributo la guarda volvia en silencio: las mismas dos entregas con fecha declarada e invertida DETENIAN comparar_evaluaciones() por el camino de medir() y publicaban delta = -1 por el camino de agregar() -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 que medir() 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_medidas viaja al agregado y al tablero, reexpresado en su clave. El «midio tres celdas de cuatro» moria en el primer salto, asi que el tablero publicaba 0,667 sin nada que lo distinguiera del 0,667 sobre cuatro. Arrastrarlo tal cual no alcanzaba: la tabla esta indexada por metrica_instanciada -Formato@t.cod- y el agregado renombra la metrica a agregada: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. - promedio y promedio_ponderado rechazan los tres tipos no acotados. metrica() promete que numero_real, entero y duracion no admiten las cuatro agregaciones normalizadas. ratio y ratio_umbral lo 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 como tipo_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_declarados con el nombre de la parte que recibio cada peso, asi que el numero se rehace con lo que el objeto publica. Y partes_con_peso_cero se decide por la propiedad -«alguna parte pesa cero»- y no por el destino: antes colgaba de un atributo de cobertura de frontera, y en conjuntoEntidades, 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: mediana y desvio se calculan en otras consultas y salian calculado en la misma corrida donde minimo, maximo, media, n_ceros y n_negativos salian no_disponible. Con UBIGINT cerca del tope eso publicaba mediana = 2^64 -un numero que no esta en la columna- y desvio = 0 sobre 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.frame o matriz cuenta filas, no campos. length() de un data.frame es su numero de columnas, asi que una columna STRUCT(a, b) sobre una tabla de tres filas publicaba n = 2 mientras general$filas decia 3. Por DBI, el bloque de muestra discrepaba del resumen SQL sin que nada declarara la diferencia. Tambien alinea n con 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_dominio publicaba tres afectadas y la traza -que si recortaba- nombraba una: perfilar() emitia su propio aviso lupa_trazabilidad_incoherente -«es un problema de lupa, 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_analizadas y n_vertices_analizados cuentan el trabajo hecho, no filas de un universo, y no hay conjunto de indices del que derivarlos: se publican tal como se midieron y ?perfilar lo 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_columnas ya no sale de partir su propia evidencia. Con una tabla de TRES columnas llamadas anio+bis, mes y dia, el hallazgo publicaba n_afectados = 4 y n_evaluados = 3 en unidad columna: 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 fundia anio (provisorio) con la columna anio real. 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_numerico declara el universo donde de verdad mira. Sobre la misma columna y en la misma corrida, ceros_no_permitidos y outliers publicaban n_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 -faltantes, filas_duplicadas- siguen en 28, que es su universo real; patron_raro mira las filas con valor (27): corregido en la ronda 24. - 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 publica cobertura_diagnosticos con 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 con match(): comparaba la PRIMERA columna contra si misma, publicaba evidencia con valores que la segunda no tiene y un exacto a distancia 0 donde la distancia de los valores reales -0,162- no llega al umbral. Es la tabla que produce read.csv(check.names = FALSE) sobre un archivo con encabezados repetidos. No se compara un subconjunto: se declara en razon y no se compara nada, igual que cuando una columna queda afuera por su largo. - Un valor declarado bytes ya no se iguala al literal de su escape. El rendido de publicacion lo convierte en a\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 salia exacto_normalizado con igualo_normalizar = TRUE incluso con normalizar = 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 en latin1 sigue siendo el mismo valor. - El separador de concatenacion no puede salir de un valor. c1 = "x | y", c2 = "z" y c1 = "x", c2 = "y | z" producian la misma cadena "x | y | z": dos filas que no comparten ningun valor salian exacto_normalizado a 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 aproximado a 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_grupos reconcilia 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 nueva grupo_perfilado dice de que clase de grupo habla cada fila, y el @return escribe 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_filas no se perfila, y la fila lo dice; la primera version del arreglo la dejaba fija en TRUE y 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, D y filas solo en A y B, la salida decia n_grupos = 2 y 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 cualquier min_filas, y min_filas promete que eso se declara. - La etiqueta de un grupo vuelve a su valor. as.character() sobre un doble usa 15 cifras significativas: 1e17 y 1e17 + 32 daban 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 y as.numeric() de la etiqueta recupera el valor exacto. Para la enorme mayoria de los numeros la etiqueta es la de siempre: 0.1 sigue 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 ejemplos completa, 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 por minimo y maximo-, 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 tests no 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_calidad al 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: medicion aguanta que le falte cualquiera de sus diecisiete columnas y un plan_limpieza recortado pierde su clase, de modo que ahi el mensaje ya era el deliberado. - winsorizar_outliers sobre una columna entera declara que deja de ser entera. Los limites de Tukey son cuartiles: la columna salia numeric con la fila diciendo destructiva = FALSE y ningun campo del registro nombrando el tipo, mientras la misma accion sobre Date sale bloqueada. Ahora se marca destructiva, la justificacion lo dice y parametros publica tipo_original y tipo_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 sin selector reescribia decision_grupo de recomendada a desactivada: 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 celdas latin1 devuelve 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 publicaba configuracion_modelo / no_comparable con severidad error sobre resultados identicos. El orden de los atributos de 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 metodo de medicion que declara quien llama en instanciar() pasa a ser parte del modelo. Dos corridas con metodos distintos daban la misma configuracion_modelo y 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, sin campos_no_comparables, sin diagnostico- y el resumen contaba los campos comparados como si fueran el universo completo. Ahora queda en campos_no_comparables, con motivo campo_solo_en_anterior o campo_solo_en_actual en detalle_campos_no_comparables y su fila en cobertura_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() y analizar() ya no mueren sobre una columna cuya clase R declara numerica pero cuyo cuantil interpolado no se puede calcular -lubridate::Period es 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, mientras perfilar() sobre la misma tabla sobrevivia. Ahora la condicion se MIDE intentando el calculo: la columna conserva sus filas con valor en NA y 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 Inf en una columna de fechas, el resumen se calculaba sobre los finitos y la fila publicaba n_valores_excluidos_resumen = 0 con estado calculados. Ademas el conteo de fechas resumidas estaba escrito dos veces -sum(!is.na(x)) para Date, sum(is.finite(x)) para POSIXt- y sobre el mismo dato una decia 3 y la otra 2. Ahora las dos dicen 2 y las dos declaran la exclusion. - integer64 sin ningun valor que sobreviva dice sin_valores. Con todos los valores declarados centinela publicaba calculados_sobre_valores junto a minimo, media y desvio en NA: el estado se escribia apenas habia excluidos, antes de saber si quedaba algo. La rama de dobles sobre el mismo dato ya decia sin_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 publicaba longitud_media = 4.5 -el promedio de DOS- y ningun campo lo decia. El campo nuevo n_longitudes_resumidas declara sobre cuantos se calcularon, igual que n_fechas_resumidas declara el universo del resumen de fecha; la causa ya viajaba aparte en n_codificacion_invalida. La ventana que no puede gobernar se declara - senal_redundante(ventana = ) promete una tolerancia, y en detectar_discordancias() esa tolerancia solo se aplica si todas las columnas comparadas son numericas. Un Date no es is.numeric(), asi que las columnas temporales caian en la rama de igualdad exacta y la ventana declarada no se usaba: medido, dos columnas Date a un dia de distancia con ventana = 1 publicaban n_discordantes = 2, contra 0 del control numerico con las mismas distancias, y la fila publicaba ventana = 1 al lado de una comparacion que no la habia usado. Ahora se avisa, y el aviso dice que hacer: llevar las fechas con transformacion a 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 con comparacion textual exacta. La documentacion de ventana lo dice ahora en el @param, donde se lee antes de usarlo. - La prueba incluye el camino que el aviso recomienda: con transformacion a 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() trae fecha_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 = error sin 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_acceso lo 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.R corre ahora con el reporte del check y uno de progreso, asi que el testthat.Rout del .Rcheck trae una linea por archivo con su tiempo: v | 178 | afirmaciones [3.0s] v | 2 54 | normalizacion Por 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 check sobre una copia del paquete con una prueba que debe fallar -da Status: 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 gsub por 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 era hallazgos$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 volvia dato [valor [valor protegido]egido], con egido publicado 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 gsub por llamada- y, medida, dejaba de enmascarar el texto marcado latin1, 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 texto latin1, 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_medicion promete que «cada metrica valida los campos que necesita y se abstiene si faltan». Las cuatro metricas ligadas a vigencia() -OportunidadEntPorFecha, OportunidadEntPorIntervalo, DesactualizacionPorFecha y DesactualizacionPorCambios- llamaban stop(), y medir() 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 y cobertura_metricas publica una fila con estado contrato_incompleto, el campo que falta y como declararlo. evaluar() deja el resumen en NA, 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 Date se ancla a la medianoche UTC y un POSIXct vale por su instante, asi que una actualizacion del dia del limite puede contar como tarde, y si el POSIXct se leyo sin huso el resultado depende de la sesion. Medido con los mismos datos y el mismo contrato: 1 0 0 con TZ=UTC y 0 0 0 con TZ=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() deja NA- 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, porque medir() 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, OportunidadAtributoPorIntervalo y las dos de GradoOportunidadAtributo*- comparaban igual y callaban: medido con una columna Date y un fecha_limite POSIXct leido sin huso, la fila que cae exactamente en el limite daba 1 1 0 con TZ=UTC y 1 0 0 con TZ=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 0 contra el 1 1 0 del control en ValoresPosiblesPorExtension -dominio-, ValoresPosiblesPorComprension -rango-, Formato -diccionario- y las tres metricas referenciales -clave del padron-, donde la cobertura daba 0 en vez de 1. Y el peor de todos: NoNulo con un centinela POSIXct sobre una columna Date con 1900-01-01 como ausencia disfrazada publicaba 1 1 1 -no falta nada-, contra el 1 0 1 del 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 OportunidadEntPorFecha decia «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_metricas con sin_valores- y el caso parcial pasaba en silencio: el tablero publicaba valor = 0.667 con universo = "celdas", que no se distingue de un 0.667 sobre las cuatro. Ahora la medicion publica alcance_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 coleccion lleva 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 publica entidad = tres_tablas y el promedio de dos, con advertencia_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_ponderado se emparejaban solo contra objeto_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 escritos c(padron_a = 0.5, padron_b = 0.5) -los mismos nombres que organizacion(colecciones = ) exige- se rechazaban, y habia que escribir c("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 llamadas t1, t3 y u1, u2 se 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 publicaba resultado = 0.9 y 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 con unique(), y esa operacion aborta en R 4.1 -el minimo que declara el DESCRIPTION- sobre una cadena marcada bytes: perfilar() se caia sobre una columna latin1 valida. 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 en attr(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 con write.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 deja resultado en NA y marca objeto_medible con [valor suprimido], que es la convencion que esta tabla ya usaba para el nivel medida; 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 marcada bytes abortaba en R 4.1 -el minimo que declara el DESCRIPTION- 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 marcada bytes, que en 4.1 abortan. Ahora afirman siempre la cardinalidad medida y contrastan contra unique() 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 decia minimo = 9007199254740992 y maximo = 9007199254740996, dos valores que ninguna fila contiene, con estado_resumen_cuantitativo = "calculados". La misma columna guardada como integer64 recibia la respuesta correcta desde el primer dia -medidas en NA, extremos exactos y omitidos_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_resumen promete contar todo valor presente que no sostiene el resumen y que, siempre que ese total sea mayor que cero, el perfil agregue una fila resumen_cuantitativo a 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 NA en ese campo mientras contaba tres en n_fechas_excluidas_granularidad. Y una columna con tres fechas de dia ambiguas mas un periodo de mes publicaba granularidad_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_personales es 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() y detectar_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 que perfilar(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"- y perfilar_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, POSIXt y difftime junto a integer64, units y Period y 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 probar is.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_reversibles promete 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 que convertir_mayusculas, convertir_minusculas, convertir_titulo, convertir_segun_diccionario, decodificar_entidades_html, reemplazar_separadores y reparar_codificacion publicaban "0 irreversibles" sobre columnas donde dos valores distintos habian quedado en uno. Medido sobre ana | ANA | Ana | beto: la mayuscula funde dos celdas y el registro decia 0, mientras eliminar_controles_invisibles decia 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 sin permitir_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 de aplicar() lo declara igual. - Un perfil_dbi trae dos coberturas y su impresion nombraba una. La otra -que diagnosticos no se evaluaron sobre la muestra- quedaba invisible, porque cobertura() 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: cafe con tilde y cafe distan 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 y c("domicilio", "nombre") publica 10, con el mismo umbral. Declarar columnas vuelve 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 en bloquear_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_vocabulario y relacion_orden_columnas no tienen estrategia en el plan, y faltantes y tipo_declarado_distinto tienen una que puede no proponer nada. Leer el plan se leia como "no hay nada que hacer ahi". Ahora viajan en attr(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 atributo pares_fuera_del_marco los 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 por perfil_mide. - El catalogo de AGESIC publica en implementacion una 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, y u, 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 unidad columna que heredan del hallazgo: sobre una tabla de 80 filas y 7 columnas se leia n_afectadas = 80 con unidad_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 marcada bytes dejaba de estarlo al normalizar: trimws() y gsub(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() y split() sobre una cadena marcada abortan con "translating strings with bytes encoding is not allowed". La capa de modelado declara sus limites - comparar_equivalencia() publica no_comparable cuando un campo no tiene valor medible en ambos lados o solo lo tiene en uno, y lo cuenta separado de identico. - 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 cuando conjuntoEntidades hace un promedio sin pesos y no conoce el alcance de sus partes. Desde coleccion siguen 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. conjuntoAtributos conserva implementada = 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 su valor_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 print del perfil y del plan declaran cuantos campos muestran de los que el objeto tiene, y donde estan los demas. El del plan ocultaba en silencio evidencia y justificacion, que son lo que se lee para decidir si aplicar una accion. La perdida se mide en el resultado - n_no_reversibles contaba 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 de PRO<U+00AD>DUCTO-A deja 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_espacios tenia 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() usa MIN y MAX del universo completo de cada tabla. Un rango calculado solo sobre la muestra ya no puede publicar sin_coincidencias; si el motor no entrega el agregado, el par se compara sin esa poda. - Cada fila de resumen_coleccion declara el universo de las metricas que alimentan sus agregados, para no confundir un conteo de la tabla completa con proporciones observadas sobre muestra_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 = 3 ninguna consulta la rechaza el motor. - estimar_costo() declara la corrida que esta estimando: modo_comparacion es el que publicara detectar_duplicados_aproximados() con esos argumentos, y filas_previstas, filas_totales y estrategia_prevista dicen sobre cuantas filas se va a comparar. Antes afirmaba exhaustiva_por_bloques siempre, y con los mismos argumentos la corrida podia comparar 447 filas de 5000. - 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 cobertura no_corroborada que 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 decide poder_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 responde TRUE a is.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 en cobertura_diagnosticos con su clase y con como evaluarla. meta$aritmetica_columnas publica 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_ambigua los 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_resumen los suma, el estado pasa a calculados_sobre_valores y cobertura_diagnosticos recibe su fila con el desglose. Una columna sin un solo valor utilizable publica sin_valores en vez de calculados con todo en NA. - El denominador de outliers, ceros_no_permitidos y negativos_no_permitidos descuenta tambien los ausentes comunes, no solo los valores que no pudieron convertirse: una columna de mil filas con cincuenta NA publica n_evaluados = 950. - eliminar_controles_invisibles declara en su justificacion que quitar el caracter no deja forma de reconstruir la celda y que el registro cuenta cada una en n_no_reversibles. La documentacion de planificar_limpieza() precisa que destructiva marca 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 y tablero_calidad() lo reutiliza para publicar la cobertura completa. El historico guarda tambien el nombre y los pares dimension-factor del marco, junto con el tipo_resultado de cada metrica. Si cambia el marco o un tipo, la deriva lo declara no_comparable con severidad error y explica el motivo. - La deriva ya no declara resuelto un hallazgo que cuenta filas de la tabla si cambiaron las columnas: publica no_comparable con severidad error y nombra las columnas aparecidas o desaparecidas. La documentacion de marcar_filas_duplicadas aclara 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() y comparar_evaluaciones() publican en cada fila la fecha de cada corrida, pero no las miraban: con los argumentos cambiados, un hallazgo que se agravó salía atenuado, un patrón que apareció salía desaparecido y 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 en meta$fecha_declarada cuá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_afectados cambiado a mano salía por hallazgos() 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 que muestra acota dentro de cada grupo. Los argumentos que acotan el trabajo viajan a cada grupo, así que con muestra = 100 y 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_consultas bloquea 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 con localizador = "indice_fila" y alcance = "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) dejaba datos_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 cuyo datos es NULL. - Lo indeterminado deja de contarse como «no corresponde». La documentación de aplicabilidad promete 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 hallazgo valor_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 en cobertura_diagnosticos y la columna publica n_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 llamada diferencia_relativa que 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 de 0,001 que pasa a 0,4 salía equivalente con tolerancia 0,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 llama diferencia_normalizada, el motivo dice 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/NA en todas las corridas, al lado de un chi-cuadrado con su p-valor: pasaba las nueve proporciones juntas a un formateador que devolvía NA para 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 columna integer64. Las acciones de marcar o eliminar ausentes respetan el universo declarado por aplicabilidad y 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_tabla deja las metricas de magnitud en no_disponible. La muestra trae esas columnas como texto y las convierte a integer64 cuando bit64 esta disponible, para que los distintos y los diagnosticos de identidad sean exactos; sin bit64 quedan como texto y la cobertura publica el remedio. Los estadisticos de magnitud de la muestra usan omitidos_precision. Pedir integer64 al conectar tambien conserva la medicion exacta. Los double, real y float declarados 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ía tabla2, y una columna llamada tasa{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 declarado bytes ya 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 de perfilar_dbi() como una lista y terminaba culpando a alguno de sus componentes; comparar_perfiles() pedía «un objeto producido por perfilar()». 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 = 2 donde 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ía tipo_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 en cobertura_diagnosticos. - Una columna constante de fecha-hora ya no acusa al paquete. La traza del hallazgo constante comparaba 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 de lupa» sobre datos corrientes. - reportar() ya no aborta con texto sin marca que no es UTF-8, el que deja read.csv() sin fileEncoding sobre un archivo latin1. Se publica como lo muestra la consola, igual que el texto declarado bytes; 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: una S/D en 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 bytes hacía que reemplazar separadores dejara ilegibles las celdas latin1 vecinas, 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 deja read.csv() sin fileEncoding sobre 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 declaradas bytes que 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 a 0.1 sin 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, NC y ND, 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 declarado bytes. - 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_regional convertía 9007199254740993 en 9007199254740992 —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 mezcla 10% con 0.1, el plan medía cinco valores irrecuperables y decía «Se declara destructiva» en su motivo, pero la marca quedaba en FALSE. Activada a mano, el aviso de acciones destructivas no aparecía y el registro repetía FALSE. - Un marcador de ausencia que podría ser un dato ya no se convierte solo. NA es el código de Namibia y NULL puede 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_invisibles y normalizar_espacios_invisibles reemplazaban el valor por la cadena literal "NA" cuando no podían decodificarlo: sobre una columna que mezcla latin1 con UTF-8, tres de siete valores. Y recortar_espacios convertía texto latin1 válido en bytes inválidos, porque trimws() sobre un vector que contiene una sola cadena marcada bytes devuelve marcadas bytes tambié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 factor o character. 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 como 1.000 sobre 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 severidad error cuyos 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 bytes se 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 publicaba ni\xc3\xb1o y 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: latin1 no es bytes —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 decir ni\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/minusculas recibían el texto sin marcar, así que sobre un valor declarado bytes toupper() no tenía cómo saber que eran caracteres y trabajaba sobre octetos sueltos. Y deriva_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 orden desempataban 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 resuelve id_accion, que no cambia al reordenar. Y un grupo marcado como elegida sin 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 el estado en calculada. Medido sobre una columna de cuatro filas con tres inválidas, la tabla publicaba z con proporción 1.00 cuando z es el 25 % de la columna. Ahora ese caso se declara calculada_parcial, y la documentación dice sobre qué base están calculadas las proporciones. Un NA no cuenta como descarte: es una ausencia declarada. - reportar() ya no aborta, y el resultado de comparar_equivalencia() se puede imprimir. Los dos morían sobre un valor declarado bytes —nchar() no puede medir caracteres ni ancho en esa codificación—, el primero con «number of characters is not computable» y el segundo al print(), 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 bytes producía un texto que el usuario puede escribir —los cuatro bytes de año y el texto literal de diez caracteres que los describe daban la misma clave—, así que n_distintos volvía a contradecir a unique() 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 y lupa publicaba 2, mientras su propia consola los mostraba separados. Ahora la identidad los separa —y n_distintos coincide con unique()—, la evidencia de duplicados conserva la declaración —se publica a\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 declarado bytes cuando 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 sobre as.character(), así que las cadenas "TRUE" y "FALSE" la pasaban y saveRDS() las rechazaba después con invalid '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 bajo LC_CTYPE=C anota ANSI_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— y Básico vuelve como BC!sico. Texto plausible, silenciosamente distinto: acumular_historico() llegaba a rechazar su propia corrida guardada. guardar_historico() y guardar_analisis() escriben ahora con version = 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 de comparar_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 latin1 como bytes inválidos y escapaba su contenido, así que el mismo texto daba dos claves según la marca —=B%C3%A1sico marcado UTF-8 o sin marcar, =B%5C341sico marcado latin1—, bajo cualquier locale. Importa fuera del laboratorio porque Windows marca latin1 donde 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 = 0 se registra como fallida aunque n_afectadas sea NA, 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. ligaduras y ancho cumplen ahora igual bajo LC_CTYPE = "C" y bajo un locale UTF-8. - La evidencia que publica decimales fijos usa la misma marca de OutDec que las columnas: sprintf() ya no deja puntos mezclados con comas en un mismo perfil. - El diagnóstico casi_duplicados_vocabulario queda 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 con universo = "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, y meta$materializacion/meta$bloques dejan el estado no_solicitado y filas_vistas = 0 cuando corresponde. - Las lecturas DBI marcan como UTF-8 los bytes válidos antes de perfilar o guardar texto; las comparaciones de NoNulo, Formato y ValoresPosiblesPorExtension usan claves por bytes. La evidencia de duplicados aproxima nombres y valores sin depender de LC_CTYPE, mientras los nombres de columna publicados conservan sus bytes y su marca de entrada. - Las secuencias integer64 registran los métodos de bit64 antes de medirse aunque el paquete no esté adjunto; si bit64 no está instalado, sus cinco medidas quedan en NA y cobertura_diagnosticos declara 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 scipen y de digits, y conservan OutDec. - La clasificación de posibles datos personales usa esa representación fija, así que scipen y digits ya no cambian la clasificación ni borran de meta los centinelas numéricos declarados. Las claves de deriva e histórico comparan nombres y configuraciones por bytes, incluso después de saveRDS() 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 cli marcan en un único punto los textos UTF-8 válidos que vienen de nombres, valores y motivos del usuario. Bajo LC_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 de factor()/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 revisaron match.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 \uXXXX dentro del código R. Esto permite que R CMD check re-encode los ejemplos bajo LC_ALL=C sin cambiar el texto que muestran marco_calidad(), modelo_calidad() ni metricas_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 reetiqueta names(): los nombres son datos del usuario y .nombres_para_operar() alcanza para las comparaciones y ordenamientos internos. Así, perfil$columnas$columna y los nombres que publican claves, relaciones, dependencias, hallazgos, patrones, remediación y reportes siguen indexando la tabla de entrada bajo cualquier LC_CTYPE. - El manejo de nombres de columna dependía de LC_CTYPE en 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 con read.csv(check.names = FALSE) o con fread(). 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 raw rompía distribucion_valores() y analizar() dentro de rbind, con un error de R que se filtraba al usuario, mientras perfilar() la aceptaba. .texto_analizable() promete texto y ahora lo cumple también para raw (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 cadena unknown y otra UTF-8 son idénticas bajo un locale UTF-8 y distintas bajo C, 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 bajo LC_CTYPE=C por un nombre de columna con UTF-8 válido marcado como unknown. El marcado de entrada ahora cubre también names(datos), no sólo los valores. - Encoding() == "bytes" no distinguía bytes arbitrarios de UTF-8 válido. Las dos APIs públicas, perfilar() e inferir_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_moda y 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 en NA y la bandera declara que no se evaluaron; n_codificacion_invalida y 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 con Encoding() en unknown —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_texto publicaba 0 cuando sí había números escritos como texto. Con tres de cuatro valores numéricos-como-texto, el campo decía «ninguno» al lado de proporcion_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:1000 más 2001:3000— el 999 que 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 con cardinalidad = "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 usaba tolower(), que en ese locale no baja los acentos: CAFÉ y café 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 la I turca. - La deriva decidía el veredicto con la resta en coma flotante. 0,70 - 0,65 da 0,049999999999999933 y 0,75 - 0,70 da 0,050000000000000044, así que dos pares que publicaban el mismo delta = 0.05 recibí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 para delta == 0 no corría nunca: usaba isTRUE() sobre un vector con un elemento por par, que con más de un par devuelve FALSE incluso con todos los deltas en cero. El NA ahora 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 de medir() y quedó sin filas —ninguna métrica aplicable, cosa que medir() declara en su cobertura_metricas— era rechazada con «debe ser un data frame no vacío producido por medir()». Las tres puertas —evaluar(), tablero_calidad() e indice_calidad()— ahora separan los dos casos y propagan el motivo que medir() ya había declarado. - Dos puertas más publicaban valores que perfilar() enmascara. La evidencia de detectar_discordancias() citaba las filas discordantes con sus valores crudos —documentos, cuando la columna lo es—, y los ejemplos de descubrir_patrones() llamada suelta salían enteros. No fallaba la capa de protección: esas dos funciones nunca habían pasado por ella. Ahora detectar_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—, y descubrir_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_densa gobernaba 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_distinto y 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 a 999 de ser una ausencia codificada apareciendo una sola vez, en su lugar, dentro de 1: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_numericos se comparaba con identical() 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() y perfilar_dbi(). - relaciones_coleccion() publicaba el mínimo y el máximo de una columna de documentos. El campo detalle de una poda por rangos disjuntos explicaba la poda escribiendo los cuatro extremos crudos —"[45120001, 45120040] y ..."—, mientras perfilar() y perfilar_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-314 con estado sano, porque el dato ya venía así del origen y ninguna comprobación cruzada podía contradecirlo. El hallazgo valores_subnormales los cuenta, da sus filas y los marca como sospechoso. No es concluyente y no lo afirma: un cálculo que desborda por defecto cae legítimamente en ese rango —2^-1050 da 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, mientras promedio sobre 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 que detectar_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 continua con confianza 0,65, la misma cifra que sobre mil observaciones. Ahora dice desconocida. Donde la clase determina la escala —logical es binaria, Date es temporal— la propuesta se mantiene. - medir() no tenía por dónde recibir qué proteger. Aceptaba proteger_datos_personales pero no columnas_personales ni validadores_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 en meta. Subir umbral_faltantes_error de 0,4 a 0,9 sobre la misma tabla publicaba severidad_hallazgo / atenuado y nada más: el efecto sin la causa. Ahora una fila configuracion_umbrales la 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 doble de los dos lados y borra justo la diferencia que había que ver. Ahora se lee el tipo declarado y la columna queda en columnas_no_comparables con el motivo tipo_cambiado:texto_vs_no_texto. factor cuenta 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 POSIXct guardada 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 con tipo_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 en America/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.5 como número y -1 como texto. Ahora moda queda en NA y frecuencia_moda se 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_tabla y la muestra— dicen lo mismo, que era el riesgo real de arreglar una sola. La clase perfil_dbi se 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 el 999 repetido 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 usa valor_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:60 con cinco -9 y un 7 repetido 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, donde rep(1:40, each = 4) con cien -9 callaba 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ía faltantes_disfrazados: un grupo emitía faltantes —«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 cedula estaba protegida y contenía "S/D", ese texto se enmascaraba en todo el perfil, incluida la columna sexo —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, y general, 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 llamada codigo_operacion y 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 discriminante debil publicaban dos documentos exactos sobre doce filas. El vocabulario corto no entra en el piso: "S/D" sigue publicándose en sexo. El piso alcanza además la forma sin separadores —"771.771-01" es el mismo documento que "77177101"— y lo aplican también analizar() y distribucion_valores(), que protegían sólo por columna: sobre la misma tabla, perfilar() tapaba el documento adentro de un texto libre y analizar() lo publicaba en variables$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, severidad error, en las dos direcciones —la misma forma que ya tenían configuracion_umbrales y configuracion_normalizacion—. No era una fuga: quien corre esa comparación ya tiene el perfil sin proteger. - Una columna llamada sep abortaba la corrida. Los nombres de columna son datos del usuario y llegaban como argumentos con nombre a paste, así que una columna llamada sep, collapse o recycle0 chocaba con sus formales y reventaba detectar_duplicados_aproximados(), detectar_claves() y perfilar(), 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() y perfilar_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 en resumen_tabla los estadísticos de orden de una copia numérica que perfilar() 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: cli emite su salida como condiciones de mensaje, y dentro de testthat quedan atrapadas antes de llegar a ningún flujo —es lo que le permite sostener expect_message()—, así que una prueba que usaba capture.output(..., type = "message") o sink() recibía un texto vacío y afirmaba que la salida no contenía lo que sí contenía. Ahora las recoge salida_cli(), que además quita los códigos de color y normaliza el espacio: cli ajusta 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 mostraba cobertura_metricas —qué métrica no se pudo medir y por qué— ni cobertura_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— y analizar() 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 que perfilar() documenta y acepta. La puerta de entrada exigía heredar de data.frame antes de delegar en su propio motor. Ahora la matriz llega a perfilar() sin convertir, para que sea él quien la convierta y lo declare en meta$entrada_convertida: convertirla en la puerta dejaba esa declaración en NA y 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 columna proteccion_temporal que gana temporal$resumen cuando una columna temporal está protegida. - La cobertura moría en el consumidor siguiente, en dos saltos distintos. medir() declara en cobertura_metricas qué métricas no se pudieron medir y por qué —«la entidad dependiente b tiene cero filas»—, y agregar() 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. Y agregar() a nivel colección adjunta cobertura_coleccion —tablas declaradas, tablas en el número, tablas sin medir y una advertencia—, que ni tablero_calidad() ni indice_calidad() conservaban: una colección de dos tablas con una vacía publicaba 0,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_hallazgo que 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 cada tipo_hallazgo citado contra los que esa corrida produce. - perfilar_por() perdía las filas del grupo en blanco. x[[""]] devuelve NULL aunque 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ía NULL, quedaba con cero filas y se publicaba como «El grupo tiene 0 filas». Sobre 200 filas con 30 en blanco, la suma de n_filas_grupo daba 170 y esas 30 filas no aparecían ni perfiladas ni declaradas. Se recorre por posición. - La moda de una columna integer64 dependía de lo que el usuario tuviera cargado. bit64 no 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 un integer64 —incluso x[1]— lo degrada a double reinterpretando sus bits: la moda publicada era 4.4501477170144e-308 sin bit64 adjunto 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 columna double equivalente. - 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 en valores_evaluados y valores_totales. - Los hallazgos del resumen cuantitativo publicaban el denominador entero. outliers, ceros_no_permitidos y negativos_no_permitidos se calculan sobre los valores que llegaron a número, pero declaraban n_evaluados igual al total de la columna: mil filas con cien que no convierten publicaban n_evaluados = 1000 sobre 900 valores evaluados. El alcance ya estaba declarado en n_valores_excluidos_resumen y en cobertura_diagnosticos, pero quien lee una fila de hallazgos no está obligado a cruzarla con otra tabla. El paquete ya hacía este mismo ajuste cuando hay un universo declarado por aplicabilidad; 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 fila alcance_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 el nzchar() que gobernaba la cuenta lo descartaba, así que una columna con 900 valores "10" y 100 en blanco publicaba la media sobre 900 filas con n_valores_excluidos_resumen = 0 y estado calculados. Los mismos 100 valores como texto ilegible sí se declaraban. Un blanco no es una ausencia declarada —el paquete deja n_faltantes en cero y lo cuenta en n_blancos—, así que su exclusión se declara como cualquier otra; NA sigue 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_dias decí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 dice calculados_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 sin clave informaba clave_no_unica como resuelto con severidad ok: 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$clave guarda el resultado de comprobarla y queda vacío cuando sale limpia. El perfil ahora registra meta$declaracion_clave y meta$declaracion_aplicabilidad, y la comparación distingue las dos cosas. - Cambiar aplicabilidad entre 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 con c() 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í que caf<0xe9> salía como los siete caracteres ASCII caf<e9> con estado = "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 reglas Resultado > umbral sin consultar la orientación de la medida, así que sobre una métrica orientada a defecto —donde más alto es peor— la regla quedaba invertida: una tabla 100 % duplicada da EntidadDuplicada = 1 y 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 argumento orientacion. Ahora la fábrica lo usa, e invierte con 1 - valor, la misma convención que tablero_calidad() aplica a sus componentes de defecto. Las métricas conformidad no 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 antes Resultado > 0.5 devolví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 integer64 cambiaba lo que el paquete publicaba sobre ella. Tres desvíos con la misma raíz, encontrados comparando la misma columna descrita por dos caminos: - bit64 enmascara order() cuando está adjunto, y dentro del espacio de nombres de un paquete no lo está: ahí order() es el de base, que sobre un integer64 ordena por los bits del double subyacente y manda los negativos al final. Con eso, ante un empate la moda de una columna integer64 salía 1 donde la misma columna en double daba -999 —y las tres puertas del perfil DBI heredaban la diferencia—. Ahora el orden pasa por un ayudante que usa bit64::rank.integer64(), correcto también por encima de 2^53. - La detección de numeración densa aceptaba entero y doble pero no integer64, que es la tercera forma de guardar lo mismo. Sin numeración detectada, el 999 de una columna 1:1000 dejaba de estar protegido por «esto es un identificador» y el mismo dato publicaba faltantes_disfrazados donde en entero publicaba posible_identificador. Por encima de la precisión de double no se mide y se declara, en vez de inventar una densidad sobre números redondeados. - La ley de Benford excluye integer64 por tipo desde el primer día, pero no lo declaraba: la misma columna producía una fila de cobertura en entero y silencio en integer64. Un diagnóstico que no corre se declara, también cuando no corre por el tipo. - n_cambiadas contaba valores presentes y no cambios, en las dos conversiones. Catorce acciones de la capa de remediación cuentan los cambios reales; convertir_tipo y convertir_fecha_confirmada contaban sum(!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ía ejecutada con n_cambiadas = 4. Ahora cuentan los cambios, y con eso la acción sin efecto se registra fallida con 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 de bit64 para duplicated() ignora fromLast, 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 en double marcaba 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_conteo se heredaba del hallazgo pisando lo que la acción declaraba, así que convertir_tipo —que cuenta valores presentes— quedaba con unidad_conteo = "columna": el plan decía «5 columna» sobre una tabla de una sola columna, y guiar_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 y sort() conserva ese orden en los empates, así que el desempate quedaba en manos de la máquina. Con max_patrones = 2 y un empate por el segundo puesto, la misma tabla publicaba A+ a+ en una sesión y A+ Aa en 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 listaba SIN DATO y 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 usaba method = "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 en columnas_omitidas, y columnas_analizadas seguí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, con n_fechas_excluidas_granularidad y estado_resumen = "calculados_sobre_dias", el mismo vocabulario que usa perfilar(). Cuando ningún formato confirmado nombra un día no hay serie diaria que construir, y entonces la columna se declara en columnas_omitidas y columnas_sin_serie_diaria en 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 los NA. 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 en cobertura_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í que columnas_personales, columnas_opcionales, clave o aplicabilidad nombrá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 de columnas_personales es 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_nac no 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_fall queda deliberadamente fuera: fall es 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— mientras perfilar() 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 atributo etiquetas_personales, con el tipo clasificado y la salida recomendada —agrupar por una columna seudonimizada—. Con proteger_datos_personales = FALSE no 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 informaba n_fechas_excluidas_parseo = 0 con estado_resumen = "calculados" sin muestrear y 50 con "calculados_sobre_fechas_parseadas" con muestra = 50, mientras perfilar() 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. Un NA sigue 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, a print(), 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() y tablero_calidad() no habían heredado la cobertura de métricas. El arreglo que impide publicar un 1 sobre controles que no corrieron llegaba a medir() y evaluar() y no a estas dos. Las tres salidas llevan ahora cobertura_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 declara columna_solo_en_anterior y columna_solo_en_actual en cobertura_diagnosticos con su como_resolverlo, y el resumen cuenta comparados y no comparables por separado. - El motivo faltante_misma_clase ya 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.5 a 0.9 producía una deriva de 1 a 0 con severidad error; cambiar sólo la aplicabilidad daba 0.5 a 1 publicado 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ía comparar_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 informaba 1 y 2. - 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 dan 0.5 porque 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 a historico_calidad(), que la documentación promociona como exportable con write.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 informan NA en 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 ejecutada sin 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ía ejecutada, n_cambiadas = 0, sin error, y el NA seguía ahí. Ahora la dependencia se resuelve sobre los datos que recibe aplicar(), 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 queda fallida con su motivo. Que lo prometido y lo hecho no coincidan tiene que ser visible. - El plan declara unidad_conteo. n_afectadas heredaba el número del hallazgo sin heredar su unidad, así que convertir_minusculas anunciaba 3 —valores distintos en colisión— y cambiaba 90 filas. El número no cambia; ahora dice en qué unidad está, y print() y guiar_limpieza() lo muestran, que es donde se lee antes de decidir. - La clave de un data.table se suelta cuando la acción rompe su orden. El resultado declaraba estar ordenado por una columna que acababa de cambiar. data.table suelta 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, y perfil_muestra, que es perfilar() 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 fila divergencia en resumen_tabla$cobertura con 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: NaN e Inf, que SQL no trata como R —once métricas discrepaban—; la cancelación de coma flotante, donde {1e16, 1, -1e16} da media 0 en el motor y la verdad es 1/3; y un defecto de PERCENTILE_CONT en MariaDB sobre DECIMAL grande, que publicaba mediana = 1e+08 mientras la muestra decía 1,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_estructural publicaba 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: con filas_duplicadas mostraba documento=, correo= y telefono= 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 en NA y bbox_alcance declara no_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_granularidad cuenta 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_resumen es 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: publicaba 4 donde eran 90. Atribuir la causa exigiría rehacer la detección sobre todo, que es el costo que el muestreo existe para evitar, así que se informa NA y nunca cero. El total sin atribuir sigue estando. - La fila resumen_cuantitativo de cobertura_diagnosticos se 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 fila configuracion_sentinelas_numericos de severidad error. Antes, el mismo data.frame byte 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, aplicabilidad y columnas_opcionales son ahora argumentos explícitos de perfilar_dbi(). Entraban por ... y se aplicaban sólo a perfil_muestra, así que una misma llamada podía publicar media = 1045.09 en el resumen del motor y media = 50.21 en el de la muestra sobre las mismas filas, sin que nada lo dijera. - Cuando alguna de ellas se usa, resumen_tabla$cobertura recibe una fila degradado que explica que los agregados SQL no honran esa política y que para ese fin sirve perfil_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/%Y y %Y/%m se 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-MM es 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, con estado = "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_granularidad y el estado baja a "calculados_sobre_dias", o a "granularidad_incompleta" si son la columna entera, donde minimo_fecha y maximo_fecha quedan en NA. - No entra %Y%m. Seis dígitos sin separador son un entero, y una columna de 202305 se sigue perfilando como entero: 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_resumen haya 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" con muestra = 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_numericos distingue la lista por omisión de una política que el usuario reemplaza. Los valores que alguien declara salen de media, mediana, minimo, maximo, desvio, los conteos de signo y n_outliers; n_valores_excluidos_resumen y estado_resumen_cuantitativo declaran ese alcance. La lista por omisión —-9, -99, -999, -9999 y 999— sigue informándose como faltantes_disfrazados y no se retira de los resúmenes, porque 999 tambié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 9999 y veinte 9998— 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: aplicabilidad y un NA escrito a mano cambiaban la media, y sentinelas_numericos —que la documentación llama «la política completa»— no. Las tres coinciden ahora. - moda, frecuencia_moda, n_distintos, tasa_distintos y longitud_* siguen describiendo la representación almacenada, igual que ya contaban un Inf que los estadísticos excluyen. Por eso moda puede quedar fuera del rango de minimo y maximo: no es una contradicción sino la diferencia entre describir lo guardado y resumir lo que vale como dato, y los conteos dicen cuánto separa a los dos. - La severidad seguía la misma confusión. Con el umbral de error en 0,4, seiscientos NA de mil daban error y los mismos seiscientos declarados como centinela daban sospechoso. 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_numerico sugería agregar el valor a sentinelas_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, con estado_resumen_cuantitativo = "calculados" y cobertura_diagnosticos vacía. - Ahora esos valores se cuentan —n_fechas_excluidas_granularidad para fechas, n_valores_excluidos_resumen para números de texto—, el estado deja de decir que el cálculo fue completo, y cobertura_diagnosticos agrega una fila resumen_cuantitativo. Sin muestra no cambia nada. - analizar_tiempo() aplica el mismo criterio con n_fechas_excluidas_parseo y estado_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.R desde los CSV de benchmark/datos/, que dejan benchmark/medir_figuras.R y benchmark/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 un diff. - El medidor firma el commit con +sucio si 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_LIBS mide la biblioteca por omisión —que puede ser de hace días— y firmaba igual el commit de HEAD. Ahora los dos medidores se detienen si el sello Built de 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=1 deja medir a sabiendas y firma los CSV con +singuarda. entorno.csv publica además de qué biblioteca salió lo que se midió. - La firma se firma a sí misma. Con benchmark/_firma.R fuera de las rutas vigiladas bastaba con sacarle la rama +sucio para 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 de R/ que sí está commiteado, y quien lo instale mide un paquete al que le falta código del commit firmado. - Sin git, la columna commit queda en NA como 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.csv con 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-duplicados ahora sale de benchmark/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_costo y la viñeta perfilar-una-base decían «unos 80.000 pares por segundo» sobre valores de doscientos caracteres. Una corrida nueva de benchmark/medir_costo_texto.R sobre 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_fallecimiento por nombre, se protegen sus momentos y se informa fecha_fallecimiento_fuera_rango sin publicar las fechas observadas. Ronda de arreglos DBI contra bases reales - La muestra saturada por spool (spool_sesion_cliente con fraccion = 1) clasifica memoria_trabajo como creciente, igual que la tabla completa, en vez de dejarlo NA. - rss_maximo publica NA cuando ningún evento del vigilante trae una lectura finita, en vez de avisar y publicar -Inf. - La documentación de bloque_filas dice que perfil_muestra sigue siendo la muestra diagnóstica acotada por muestra, max_celdas_muestra y max_bytes_muestra, y que la cobertura se lee en meta$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. Con normalizacion(acentos = FALSE, minusculas = TRUE) —un perfil documentado—, bajo LC_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, en R/pliegue-caja.R, que conserva el acento y sólo cambia la caja. Comprobado comparando puntos de código: bajo C y bajo es_UY.UTF-8 el resultado es el mismo. Al auditar el mapa entrada por entrada apareció que faltaba justo U+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 a i en un locale UTF-8 y quedaba intacta bajo C. Y .normalizar_nombre_columna() seguía dependiendo del locale por un problema de orden: bajaba la caja antes de transliterar, así que bajo LC_CTYPE=C las mayúsculas acentuadas llegaban intactas y el filtro [^a-z0-9] las borraba —Ñandú quedaba en andu y VÍA_1 en va1—. - 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 de max_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ía Mİ_TABLA en vez de MI_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_NUMERIC que 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_hallazgo que publican los README no la vigilaba ninguna prueba —que es exactamente como envejeció la anterior—. Ahora test-vocabulario-publicado.R comprueba 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 \value describían el retorno como «un objeto de clase X». Ahora dicen qué trae ese objeto y qué no hace la función. - Description explica AGESIC, ISO/IEC y CEA/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_declarado publica la mezcla —"31981, 4326"— y el dominio se evalúa por grupos de SRID. crs_geografico queda NA cuando 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::sfc no 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, y tryCatch no 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 que sf lo 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 (media y media_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 en campos_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_disponible y 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; aplica politica_costo = "por_cardinalidad", registra dbfetch_bloques como estado acotado y audita filas, orden y pasadas. - La atribucion de derrames de PostgreSQL empareja pg_stat_statements por queryid, y los avisos de estimacion declaran el origen de su denominador aun cuando validos no 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 verifica muestra_id, snapshot_id, orden_id, n_filas, bytes y 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$materializacion publica backend, versión, checksum, bytes, presupuesto y estado. Un exceso medido antes de escribir cada chunk publica spool_presupuesto_excedido y muestra_inestable:presupuesto_materializacion; no se entrega una muestra híbrida. - perfil_muestra$meta$filas_analizadas, hallazgos, cobertura_diagnosticos y meta$origen_dbi$muestreo forman 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 llama id_consulta, sin alias; muestra_id queda 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 recorrer tabla_completa con un único dbSendQuery() y sucesivos dbFetch(n). Los acumuladores conservan ordinales globales y el resumen publica alcance, 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ícitamente no_disponible:dbfetch_no_incremental segú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, NaN e infinitos; un resumen SQL que no pueda leer +/-Inf puede divergir por la representación del controlador. Estimacion de derrame de moda y mediana en DBI - plan_perfilado_dbi() y perfilar_dbi() publican estimacion_derrame_moda y estimacion_derrame_mediana. La moda deriva su metodo del plan EXPLAIN (FORMAT JSON, COSTS OFF) sin ANALYZE; la mediana usa la huella de decision del sort, con pisos de 32 bytes para tipos fijos y 42 para numeric. La consolidada decide por el maximo de columna y publica la suma de tapes solo como estado_io_total_bytes informativo. - Cuando se solicita validos, la meta y los avisos usan el n_validos medido 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 publica llamadas_en_ventana cuando agrega llamadas concurrentes. Mediana y conteos bajo muestreo DBI - perfilar_dbi(universo = "muestra_motor") calcula la mediana sin dos_consultas cuando el dialecto requiere la CTE de ventanas, y publica los conteos de la muestra con sus denominadores locales. TABLESAMPLE SYSTEM declara su sesgo por bloques; el fallback NEWID() 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$sql agrega memoria_trabajo, derivada del estado de medición, del alcance efectivo y del método resuelto. Distingue trabajo creciente, acotado y 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_inferido como confirmado, candidato o NA; 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_disponible sin 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 su ORDER BY queda 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 = 7 y median() bajo el filtro explícito is.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_disponible con 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/-0 e integer64. - 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 modo de perfilar_dbi() y plan_perfilado_dbi(), junto con meta$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: exacto son los valores por omisión (universo = "tabla_completa", todas las métricas y estrategias exactas); seguro equivale a metricas = c("validos", "basicos", "desvio"); conteos equivale a metricas = "validos"; muestreado equivale a universo = "muestra_motor", muestra_motor = n y muestra = n; y aproximado se traduce como estrategia_mediana = "aproximada_motor", sin forzar el catálogo ni apagar el atajo estructural de otros ejes. La procedencia aproximada de distintos se pide aparte con estrategia_distintos = "aproximada_motor". - estrategia_mediana describe 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 queda calculado, con error_esperado = "no_aplica", aunque se haya solicitado la estrategia aproximada; sólo una aproximación ejecutada queda estimado. - 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 temprano estrategia_mediana = "aproximada_motor", porque la muestra ya es una aproximación del universo. - politica_costo acepta 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_muestra o max_bytes_muestra recorta una muestra DBI, la misma fila y el mismo motivo quedan en perfil_muestra$cobertura_diagnosticos y resumen_tabla$cobertura. perfil_muestra$meta$tope_que_mando declara si mandaron celdas, bytes o la propia muestra, 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.reltuples positivo 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; reltuples cero o negativo conserva sin 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() agrega valor_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 severidad sospechoso cuando 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 raw que no tienen un encabezado WKB plausible ya no se envían a sf; 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() agrega avisar_costo_moda con umbral_segundos_aviso_moda y avisar_costo_mediana con umbral_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_moda y meta$costo_mediana, junto con el ya existente meta$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 alcance muestra quedan en NA y con estado no_disponible; el motivo nombra la muestra vacía. n conserva el conteo de la tabla completa y cobertura mantiene el aviso de que la consulta de muestra devolvió cero filas. - No se publica 0 en n_validos o n_distintos, ni se dispara la cascada sin_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 aplica umbral_cardinalidad únicamente a la moda. El valor por omisión baja de 0.95 a 0.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_cardinalidad para omitir ambas métricas: esas llamadas ahora conservan la mediana bajo por_cardinalidad. No se agrega un segundo umbral porque no hay evidencia para omitir la mediana por proporción. meta$decisiones_costo declara 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() y perfilar_dbi() aceptan max_celdas_muestra y max_bytes_muestra, con los mismos valores por omisión: 1.000.000 celdas y 512 MiB. En DBI sólo limitan el bloque perfil_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_diagnosticos con celdas o bytes observados, umbral, motivo y cuál tope mandó. Con ambos topes en Inf no se declara un recorte. estrategia_distintos = "catalogo" en PostgreSQL - perfilar_dbi() y plan_perfilado_dbi() leen pg_stats.n_distinct y publican sus resultados como estimaciones de catálogo en los modos que miden la relación entera (exacto, seguro y conteos). Los valores negativos se convierten con la suma de pg_class.reltuples de la jerarquía que lee una consulta sin ONLY; si la relación tiene descendientes se elige inherited = TRUE y, sin hijas, la única fila FALSE. En muestreado y aproximado, la estrategia queda no_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 falta ANALYZE, el denominador de la jerarquía no es utilizable o hay ambigüedad, el resultado queda no_disponible y no se reemplaza por cero. Validación de claves - Se cierra el pendiente de una política separada con catalogo, reutilizar_recorrido y exacta: 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 columnas y, dato, llegaba a publicar clave primaria en una columna x inexistente 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 BY de una función de ventana, así que su camino nunca se activaba; MariaDB implementa PERCENTILE_CONT sólo como función de ventana y estaba clasificada con la forma sin OVER. 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 un summary(), 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 de tipo_hallazgo. - Los argumentos de medir(), evaluar() y cobertura_analisis() indican en qué posición se recibe cada objeto y distinguen el perfil descriptivo de perfilar() del perfil de evaluación de perfil_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 desde perfilar() hasta evaluar() y muestra el orden de la API. La clave declarada separa ausencias y repeticiones - perfilar(clave = ...) publica clave_con_ausentes para las filas que impiden garantizar NOT NULL, y reserva clave_no_unica para valores repetidos entre filas con la clave completa. La descripción, la evidencia, los conteos y las trazas comparten ahora el universo de meta$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_duplicadas usa la misma comparación en ambas direcciones que el conteo. Esto corrige las columnas integer64, cuyos métodos pueden ignorar fromLast, y mantiene alineados n_afectados y trazabilidad$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 como blob/vctrs_list_of de valores raw, cuyo unique() no funciona con versiones antiguas de vctrs. 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 o no_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. lupa ya normalizaba esas columnas antes de contar; el test fija ahora esa conducta estable en 1 fila duplicada y 2 filas 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 en attr(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_muestra a 109 campos analíticos además del nombre de la columna, y describe que muestra también limita la búsqueda de dependencias. El valor por omisión Inf ya no se presenta como una elección distinta de Inf; también se elimina la referencia a un max_pares que no es argumento de perfilar(). 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 de perfilar()— quedó midiendo el valor guardado mientras comparaba el normalizado. Con la normalización amplio, que expande ligaduras, una columna de 5.101 caracteres publicaba largo_excedido = FALSE y 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_maximo publica 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 en dbo, consultada por un usuario con otro esquema por omisión, se lee del motor pero el catálogo devolvía cero filas y se publicaba no_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() agrega avisar_costo_distintos con umbral_segundos_aviso_distintos, y avisar_derrame_estimado con umbral_bytes_aviso_derrame_estimado: el costo de COUNT(DISTINCT) se controla en segundos y el derrame estimado en bytes. Inf desactiva 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$derrame y meta$estimacion_derrame siguen 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 en perfilar_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 (clave id), s1.dup (a,b) y s2.dup (x), perfilar_dbi(con, "dup") publicaba columnas = id, a, x, b con garantía garantizada: 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 el search_path y devuelve la misma relación de la que se leen los datos—, DATABASE() en MySQL y SCHEMA_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 desconocida con motivo vacío. El diagnóstico era correcto —MariaDB no expone ENFORCED en information_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 casos alcance$largo_maximo publicaba 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_maximo publica 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 y largo_maximo vale NA en vez de 0: 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() consulta pg_stats.n_distinct, pg_stats.avg_width, pg_class.reltuples y work_mem para estimar el tamaño del hash de agregación antes del primer COUNT(DISTINCT); en PostgreSQL 13 o posterior incorpora hash_mem_multiplier al límite efectivo. - El aviso dice el tamaño estimado, la memoria vigente y que subir work_mem en la sesión puede evitar el derrame. La configuración nunca se modifica. El diagnóstico queda separado de meta$derrame, rotulado siempre como estimación; si luego pg_stat_statements mide un derrame, esa evidencia posterior prevalece. - Permisos insuficientes, tablas sin ANALYZE, reltuples desconocido, 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 de perfilar() 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 en alcance o en cobertura_diagnosticos; Inf recupera 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 en meta$costo_tabla_ancha una 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 = FALSE y umbral_celdas_aviso_tabla_ancha = Inf son desactivaciones explicitas. - Se agrego benchmark/medir_topes_valores_ancho.R para 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_guard junto a la frecuencia y valida frecuencia_moda <= n_validos dentro 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_guardian declara 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 solo Seq Scan y 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 LIMIT reemplaza 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 con LIMIT. - 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 de PERCENTILE_CONT(...) WITHIN GROUP no 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 de COUNT(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 usa reltuples, no cambia work_mem y 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ólo metricas = "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_statements antes y después de los distintos exactos. Sólo publica derrame real cuando puede atribuir exactamente una llamada: resumen_tabla$sql agrega los bloques temporales y meta$derrame conserva el estado, la fuente y el motivo. La falta de evidencia queda no_disponible, no se infiere del reloj. - tamano_lote_distintos conserva el valor medido 2; el cambio no modifica work_mem. resumen_tabla$sql declara su unidad sumable - resumen_tabla$sql conserva una fila por columna y métrica, pero duracion_ms pertenece a la consulta y puede repetirse en varias filas. La nueva columna nivel sigue la semántica de resumen_tabla$tiempos: nivel = 1 identifica la primera fila de cada consulta_id y nivel = 2 las filas repetidas. - La suma segura de duracion_ms usa sólo nivel = 1; las filas sin consulta siguen teniendo NA. 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 aunque estrategia_distintos esté 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 a COUNT(DISTINCT ...) para una estrategia omitida, de catálogo o aproximada sin capacidad. - resumen_tabla$meta$snapshot declara que no hubo lectura instantánea. Cuando valores exactos incoherentes de n_validos y n_distintos vienen de grupos de consistencia distintos, cobertura suma alcance_distinto y 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() y plan_perfilado_dbi() aceptan la estrategia explícita de distintos exacta, aproximada_motor, catalogo u omitida. La estrategia exacta es el valor por omisión; una aproximación o estadística de catálogo ausente queda no_disponible sin repliegue a COUNT(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 separadas estrategia_distintos y fuente_cardinalidad_costo. - perfilar_dbi() ejecuta primero los agregados planos, luego el total exacto, los distintos, la moda y la mediana. tamano_lote_planos y tamano_lote_distintos son 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.95 y omitía moda y mediana. Las dos cosas cambiaron al medirlas —el umbral es 0.5 y 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. - datos debe heredar de data.frame, once veces. perfil debe 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 como resuelto un diagnostico que simplemente dejo de correrse. - perfilar_dbi() ya la tenia en resumen_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 check avisa cuando un archivo de R/ 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 \uXXXX son la misma cadena. La unica forma de distinguirlas es mirar el fuente. La comprobacion nueva lo hace, se saltea diciendo por que cuando R/ no esta a la vista -bajo R 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 severidad error con la trazabilidad a las filas. - sugerir_clave() y elegir_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. Otra permite 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_benford mientras 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 222 entre 221 y 223 funcionaba. Lo tumbo la codificacion mas comun de los microdatos de encuesta: 9999 = "no sabe" junto a 9998 = "no contesta". Medido, el 9998 contaba como vecino y descartaba al 9999, y como el 9998 no 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/223 de 9998/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 MotId de 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 10000 entre identificadores de 1 a 100, o un ano centinela 1900 entre anos 2000-2030, bajan la densidad y vuelven a senalarse. - alta_cardinalidad sobre 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 <= MEsId se 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 un inicio/fin legitimo -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. - constante medido 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_costo decia "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 -convertir filas_leidas en 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_trabajo a 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 preserva DATE ni BOOLEAN hace 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_fecha y compania; el desvio no es un momento sino una duracion, queda como numero, y un 136610.4 sobre 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_benford explica que en muchas magnitudes el 1 encabeza cerca del 30 % de los valores y el 9 menos del 5 %, y outliers dice 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_rechazados aparece 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 severidad error contra 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. y p.m. estan en casi todos los valores; Delayed esta 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() dice latin1 cuando R conoce la codificacion, y enc2utf8() convierte sin perder nada. El paquete estaba tirando informacion que podia recuperar con una llamada. Con eso, la misma columna informa n_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 0xE9 era una e con tilde o basura— pero ahora cobertura_diagnosticos gana una fila texto_no_descifrable que 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)/2 supera 2e10, 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_trabajo recorta 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, con localizador = "ninguno" al lado. La rama vacia va a no_disponible, que es el valor por omision de la propia funcion. - .reparar_mojibake_uno estaba 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_disponible y .bit64_disponible_dbi eran la misma funcion con dos nombres. Queda un solo punto de verdad. - .detectar_orden_columnas() recibia un argumento resultados que su cuerpo no usaba, y el llamador lo construia para nada. - muestra = 1.5 se 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: Inf vale 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 CARRASCO junto a CAMINO 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 double ya representa el entero exacto. - Y agregaba tres problemas. La clase del mismo campo dependia de un Suggests. perfilar() devolvia integer para n_distintos y perfilar_dbi() devolvia integer64: dos puertas del mismo paquete en desacuerdo sobre el mismo campo. - El tercero es el que decide. Un perfil guardado en una maquina con bit64 y leido en una que no lo tiene mostraba: columna n_validos n_distintos 1 id 9.881313e-323 9.881313e-323 donde 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. integer64 se conserva solo donde compra exactitud: un conteo por encima de 2^53 que el motor entrega como texto o como integer64. Para un conteo de filas eso significa una tabla de mas de nueve mil billones. - De paso se corrige el error simetrico: conteo_exacto decia FALSE para 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 testthat declara R (>= 4.1.0). Eso es cierto contra CRAN de hoy y falso contra el snapshot de la epoca: ahi testthat 3.1.7 instala 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() trae stringsAsFactors = TRUE, y el paquete tiene 275 llamadas a data.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 queda NA. 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.0 y no 4.0.0 porque es lo que pide testthat actual: 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, pide R (>= 3.4). - Los doce fallos restantes bajo versiones viejas de los Suggests no son del R: con RSQLite 2.3.1 y bit64 4.0.5 los conteos vuelven como integer64 y 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. Sin stringi, 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_soporte armaban el data.frame con la columna ya marcada como integer64, y eso obliga a as.data.frame() a buscar el metodo de bit64. La prueba de que falta bit64 exigia que bit64 estuviera. 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: odbc resuelve dbColumnInfo() con nanodbc::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 expone column_datatype_name ni 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_comprobada distingue 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". Y sondas_descarte publica cuantas consultas costo averiguarlo. - El descarte esta acotado por los dos lados: como mucho 2n sondas sobre n columnas —que es lo que cuesta la biseccion en el peor caso—, tope absoluto de 512, y nunca mas de la mitad del saldo de max_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 metodo seguia declarando el muestreo nativo—. Ahora la lectura original y el reintento salen de la misma receta, y metodo, acotado_en y fraccion se 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_leidas y ordenaciones_completas, resumida en magnitud_motor. La del cliente esta en columnas_texto y pares_texto —cuantos pares de formas podria comparar el detector de vocabulario sobre la muestra—, resumida en magnitud_texto. magnitud es la mayor de las dos. - La unidad es el par de formas comparadas, que es una cuenta y no un indice: la muestra trae m filas, las formas distintas son a lo sumo m, y el detector nunca compara mas de max_pares por 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_costo lo 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_vocabulario vale 2e10 por 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)/2 supera 2e10, 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 apagaba max_pares para 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() gana max_trabajo, en unidades fila-par, porque ahi el costo es del orden de columnas^2 x filas y max_comparaciones no lo veia: 158 columnas son 24.806 pares, muy por debajo de las 200.000 del tope. Se combina con max_comparaciones y manda el mas restrictivo. perfilar() lo expone como max_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— y ordenaciones_completas —cuantas veces habria que ordenar la tabla entera—. De ahi sale magnitud: "baja", "media", "alta", o "desconocida" si no se conoce el numero de filas. El peso de cada clase de consulta sale de su alcance, que ya venia declarado. - Al imprimirlo, un trabajo alto viene con las palancas concretas para acotarlo —modo = "muestreado", recortar metricas, bajar muestra, 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 como log2(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 | | --- | --- | --- | --- | --- | | conteos | 8 | 6.001.000 | 0 | baja | | seguro | 14 | 26.001.000 | 0 | media | | exacto | 94 | 186.001.000 | 80 | alta | | muestreado | 94 | 2.445.000 | 0 | baja | Las mismas 94 consultas son «alta» en exacto y «baja» en muestreado: el conteo es identico y el trabajo difiere por setenta y seis veces. Contar consultas no podia distinguirlos. Y los conteos de conteos y seguro son 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$cobertura gana una fila alcance_distinto que 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. - meta tambien 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_leidas declaraba haber leido justamente la columna que no se pudo leer, y sql_muestra publicaba la consulta original en vez de la que de verdad se emitio. La cobertura decia la verdad y meta decia otra cosa, que es informar como medido lo que no se midio, en el lugar donde se mira para saber que se hizo. Ahora meta trae las columnas que realmente se leyeron, el SQL del reintento, y ademas columnas_omitidas con su motivo. - El reintento es portable: no emite conversiones propias de un motor, solo vuelve a pedir la consulta sin las columnas rechazadas. Un CAST distinto 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 | | --- | --- | --- | | conteos | 46 consultas, 5,4 s | 8 consultas, 2,4 s | | seguro | 128 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_disponible con 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$sql conserva una fila por columna y metrica con todos sus campos, y agrega lote y columnas_compartidas para que se vea cual consulta fue compartida. plan_perfilado_dbi() publica tamano_lote, y su total pasa a estar declarado como techo en attr(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 el MIN de una columna declarada DATE como el texto "2020-01-01"; as.numeric() lo convierte en NA y el estado quedaba calculado. Decir "se midio" y "no se midio" a la vez sobre el mismo campo. Ahora la metrica queda no_disponible con el valor original del motor en el motivo. Lo mismo para un integer64 cuyo paso a doble lo cambiaria: el maximo publicado no estaria en la columna. - La guarda de exactitud de integer64 tenia 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_motor la 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. Un bigint llegaba a R como integer64 y 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 incluia outliers sobre columnas que no tienen nada de espacial. Afecta a cualquier bigint. 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 respondido permiso 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 faltantes nombra la senal estructural. Cuando posible_ausencia_estructural dispara sobre una columna, el faltantes de 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$materializacion en muestra_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() y sql_perfil() leen las cuatro formas de salida del paquete sin depender de como estan armadas. perfil$general$filas funcionaba sobre la salida en memoria y devolvia NULL sobre la salida DBI, donde el conteo vive en resumen_tabla$meta$filas; un NULL silencioso 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, y sql_perfil() sobre un perfil en memoria devuelve NULL porque 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: un Can't open lib ... file not found de ODBC pasa a decir que falta unixodbc-dev y cual es la salida sin sudo. - Un controlador que no implementa dbIsValid() ya no se toma por conexion rota: se prueba dbGetInfo() antes de rendirse. El ROracle archivado es el caso, y por eso Oracle quedaba fuera de coleccion() aunque el SQL funcionara. El camino de Oracle, contemplado y sin corrida que lo respalde - Se contemplan los dos dialectos de Oracle -fetch_first y rownum-, que son los unicos que no comparten forma con los demas motores: los otros usan limit o top. El camino cubre cuatro particularidades del motor: la sonda del desvio necesita FROM DUAL; Oracle rechaza TABLESAMPLE y usa SAMPLE (p), que se agrego como forma candidata con su sonda; dbExistsTable() del ROracle archivado devuelve falso para nombres calificados aunque el SQL funcione; y una columna CLOB no 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 DATE se media como numero, con estado calculado. El dbFetch(n = 0) de RMariaDB devuelve numeric(0) para una fecha: la clase se pierde junto con las filas, e is.numeric() decia que si. Salian minimo en dias desde 1970 y media en YYYYMMDD -dos unidades distintas, las dos publicadas como la misma-. Ahora el tipo declarado por el motor manda sobre el prototipo. - Un BIGINT UNSIGNED cerca del tope daba maximo menor que minimo, los dos calculado. 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 = 0 y sin_valores sobre una columna llena, con fraccion = 1 al 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_partes y marca completo = 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_estructural no disparaba con mas de 20 niveles, o sea no veia edad >= 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 eso agregar() 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 = 0 y 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: SYSTEM devolvio 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 devuelve 0 o 2048 filas 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, severidad ok. aplicabilidad resolvia 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, tambien ok. 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_personales declara que columnas traen datos personales, con tipo o sin el. Ningun lexico de nombres puede ser completo —una columna con documentos se puede llamar cod_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_protegido dice si el valor quedo protegido, no si la clasificacion pensaba protegerlo. Con proteger_datos_personales = FALSE la moda se ve, y decir TRUE al lado de un valor visible era informar como hecho algo que no paso. La intencion sigue en datos_personales$proteger. - Un correo ofuscado —usuario at ejemplo punto com— vuelve a ser un correo para la clasificacion, aunque validar_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 frase lunes at casa no entra: el dominio tiene que traer su separador. - Una matriz de dos dimensiones es una tabla y perfilar() la acepta. La conversion queda declarada en meta$entrada_convertida. La via DBI sobre tablas que no entran en memoria - Modos muestreado y aproximado. El primero muestrea en el motor —TABLESAMPLE donde 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: estado distingue calculado, estimado y no_disponible, y cada fila lleva universo, tamano_muestra, fraccion, metodo y error_esperado, que es desconocido cuando 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 integer64 cuando bit64 esta instalado, asi que un conteo por encima de 2^53 deja de perder exactitud. Sin bit64, meta$conteo_exacto lo sigue declarando. Bases enteras, y el costo de compararlas - detectar_relaciones() y relaciones_coleccion() aceptan columnas_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 de podar = TRUE, y cuando se aplican el par no desaparece: sale con cardinalidad = "sin_comparar", coberturas NA y su motivo. Un par que no se evaluo no es un par sin relacion. - tope_memoria_mb acota las filas comparadas y declara los pares pendientes en vez de devolver menos sin decirlo. - La granularidad conjuntoColecciones pasa a medirse, con la frontera declarada por el usuario y pesos explicitos: agregar entre colecciones sin pesos seria inventar un juicio, que es lo que indice_calidad() se niega a hacer. organizacion y conjuntoOrganizaciones siguen sin implementar, y por la misma razon de siempre: no falta codigo, falta el objeto. - La entrada data.frame de coleccion() valida NA, 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_rara cuenta 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_vocabulario es 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 de casi_duplicados_vocabulario. - La evidencia de patron_raro declara desvio_unicamente_largo_corrida_numerica cuando 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_dependencias acota la busqueda de dependencias funcionales, cuyo costo es del orden de columnas^2 x filas y 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 objeto sf conserva 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, y datos[NA, ] devuelve una fila entera de NA en vez de ninguna. relaciones_coleccion() filtra con which(). Infraestructura - inst/WORDLIST completa 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.md corrige el orden de la verificacion previa. test_dir() y test_file() cargan el paquete con library(lupa), que no expone las funciones internas y produce veinte errores falsos de «could not find function»; test_check("lupa") es lo que corre R CMD check, y necesita el paquete instalado. Cuatro motores reales - El desvio se pide primero con la funcion nativa del motor —STDDEV_SAMP del estandar, STDEV en 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 top por su cuenta. Numeros que no pueden ser - perfilar_dbi() resuelve un nombre calificado con punto igual que coleccion(). 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_distintos las contaba mientras n_validos ya no, y tasa_distintos podia 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_residencia y barrio. 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, rownum y una via portable con dbSendQuery() + dbFetch(n). Se puede declarar con dialecto = 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_tabla completo, perfil_muestra = NULL y una fila de cobertura con el motivo. Antes, un motor que no acepta LIMIT descartaba 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 calculado y valor vacio, que era peor que declararlas no disponibles. - Argumentos nuevos para acotar el costo: modo, metricas y max_consultas, mas plan_perfilado_dbi(), que dice cuantas consultas va a costar el perfilado antes de emitirlas. - resumen_tabla pasa 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. Nuevo print.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 = -Inf ni n_columnas_sin_faltantes = 0: son NA, 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 con conservar_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 NA con cobertura_diagnosticos vacia, y cobertura_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 NA de 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() acepta aplicabilidad, una lista de formulas por columna que declara en que filas la columna corresponde. Las filas fuera del universo salen de n_faltantes y de prop_faltantes en 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() acepta columnas_opcionales para 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 NoNulo acepta aplicable con 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 es NA. - El hallazgo constante sobre 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_dependencias se declara en cobertura_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_raro declara en cobertura_diagnosticos cuando no puede ejecutarse porque el patron dominante no alcanza umbral_patron_dominante. La fila conserva la proporcion observada y explica como ajustar ese argumento. - La evidencia de cada hallazgo patron_raro publica la proporcion del patron dominante y cuantas filas quedaron en patrones no dominantes excluidos por superar umbral_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_raro conserva separado el tope de presentación y el alcance de la trazabilidad: resumen_patrones y 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_diagnosticos y 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, aunque is.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_vocabulario entrega primero las filas de formas no dominantes y después las de formas dominantes. La unidad sigue siendo valor_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_vocabulario conserva unidad_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 clase lupa_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_inconsistentes y normalizacion_unicode declaran unidad_conteo = "valor_distinto" y cuentan en n_evaluados los valores distintos evaluados. n_afectados ya 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_duplicadas cuenta ahora todas las filas participantes de los grupos, en línea con EntidadDuplicada y con marcar_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 NA en n_afectados y deja el motivo en cobertura_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_raro no 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. Ahora descubrir_patrones() expone n_patrones_raros y la guarda pregunta lo único que corresponde —si ese conjunto fue recortado por el tope de seis—. Sin recorte la enumeración es completa y n_afectados toma su valor real; con recorte se enumera igual y el alcance es patrones_parciales, declarado en la cobertura. - outliers, valores_no_finitos, ceros_no_permitidos y negativos_no_permitidos condicionaban 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 un character y 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_rota reimplementaba 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 alcance completo, que es justo lo que este paquete no hace—. Ahora reutiliza la máscara del detector. - patron_raro nombraba, 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 —9 frente a 9+— 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 y n_afectados recorrí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 este frente a oeste —dos puntos cardinales— como posibles variantes de un mismo valor. Medido sobre tablas limpias y sobre erratas sembradas, los falsos positivos quedan entre 1,0 y 1,5 de asimetría y las erratas reales desde 9,0, así que ahora se exige una asimetría mínima de 2, configurable con min_asimetria_vocabulario. - El piso no se aplica a los grupos formados por normalización. Montevideo, MONTEVIDEO y Montevideo son tres grafías del mismo valor y con una aparición cada una su asimetría es 1,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_diagnosticos informa 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, y perfilar_coleccion() devuelve una fila por tabla con sus agregados exactos, más cobertura_coleccion con lo que no se pudo medir. La granularidad coleccion del 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 momento en que se midió y meta$snapshot declara 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ólo promedio_ponderado: sin esa restricción bastaba pedir promedio para 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_medir con su motivo, la cobertura y 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, y estimar_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 en cobertura_pares en 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 —por survey, por calidad del INE de Chile, o por cualquier otra fuente— y las lleva al contrato de medir(), para poder evaluarlas contra un marco declarado. lupa no 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,30 y un tamaño de muestra de 0,30 no 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 lupa lo 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, y detectar_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. - transformacion lleva columnas guardadas de formas distintas a una escala comparable —extraer el año de una fecha, por ejemplo—, y ventana es 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_evaluadas lo declara. Si ninguna fila tiene todas las columnas presentes, n_discordantes queda en NA y 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() acepta umbrales, 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 de propiedades_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() acepta clave con 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 por argumentos_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, y localizador —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_protegidas declara 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_vocabulario nombraba dos diagnósticos distintos: agrupar valores por su forma normalizada, que no depende de nada, y medir proximidad por distancia de edición, que necesita stringdist. Sin ese paquete el primero medía y el segundo se declaraba bajo el mismo nombre y para la misma columna, así que cruzar cobertura_diagnosticos con hallazgos por (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_vocabulario en las tres razones que le corresponden: falta stringdist, 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_raro distingue en la evidencia las dos clases de desvío: clase_desvio=largo_de_corrida cuando el valor señalado sigue el mismo patrón con un número de otro largo —persona9@ frente a persona300@— y clase_desvio=estructural cuando es otra forma —SIN CODIGO frente a AB-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 que metrica(). 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() acepta metodo_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 columna metodo con su supuesto en supuesto, así que ninguna lectura depende de recordar cuál se pidió. analizar() lo traslada con metodo_asociacion_numerica. - Los dos README explican ahora dónde viven la distribución de valores y las correlaciones —en analizar(), no en perfilar()— y por qué esa separación es deliberada. Tres afirmaciones que el paquete hacía sin fundamento suficiente - alta_cardinalidad se 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_duplicadas afirmaba 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ó a cobertura_diagnosticos con su motivo. Cuando sí hay filas, la evidencia declara ahora sobre cuántas se comparó. - relacion_aritmetica_columnas se salteaba en silencio cuando la tabla no llegaba al mínimo de filas comparables. Ahora se declara en cobertura_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() y guiar_limpieza(). El caso que importa es data.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_par distingue ahora exacto, exacto_normalizado y aproximado; igualo_normalizar deja esa causa visible en cada fila. El hallazgo duplicados_exactos_normalizados evita afirmar que dos filas tienen los mismos valores y la trazabilidad lo busca entre los pares de ese tipo. n_pares_exactos cuenta sólo texto guardado igual y n_pares_exactos_normalizados completa la explicación junto con n_pares_aproximados. Casi-claves y precedencia de ausencias - perfilar() informa una casi_clave cuando 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 vectores double con 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 vectores integer64 cuentan como enteros semánticos. detectar_claves() las expone sin confundirlas con claves exactas, y analizar() las reitera en sus advertencias. - casi_duplicados_vocabulario retira primero los valores ya detectados como faltantes_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, exclusiones no_aplica y la advertencia de que los componentes provienen de universos distintos. - analizar() mide por omision la propuesta en estado lista, declara que no fue confirmada, agrega las medidas y conserva el tablero. El detalle fila a fila solo se retiene con conservar_detalle_medicion = TRUE; la medicion automatica se desactiva con medir_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_identificador y no recomienda convertir el texto numerico a una medida cuantitativa. - casi_duplicados_vocabulario cubre una sustitucion en valores de hasta seis caracteres cuando la variante ocupa como maximo 0.05 de la columna y la forma dominante es al menos 10 veces mas frecuente y ocupa al menos 0.5 de la columna. El limite y los tres umbrales quedan en la evidencia y se pueden ajustar en perfilar(). Orientacion explicita de las metricas - metrica() declara si un resultado expresa "conformidad", "defecto" o "no_aplica". La orientacion viaja por medir(), agregar(), reportar() y evaluar() sin invertir los valores; una regla puede recibirla como segundo argumento. El historico conserva el esquema 1 y sigue leyendo archivos anteriores. - Formato queda alineada con el factor Correctitud sintactica de marco_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_vocabulario queda como señal sospechoso sólo cuando encuentra grupos; un resultado negativo queda como ok con cero afectados, y un diagnóstico que no aplica se registra en cobertura_diagnosticos. - relacion_orden_columnas separa la escala de la relación fila a fila con un solapamiento intercuartil mínimo de 0.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() acepta desenlace = "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 en meta$benford; las que fallan se declaran en cobertura_diagnosticos y 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 URLs http y https, con esquema obligatorio por omisión, soporte para IDN y puertos, y rechazo deliberado de javascript:, data:, espacios y controles literales. - perfilar() informa unidades_mixtas cuando 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 emite monedas_mixtas con sus frecuencias, sin convertir ni suponer tasas de cambio. También informa celdas_multivaluadas só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_aritmetica se 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_aritmetica y max_columnas_aritmetica hacen visibles los supuestos y el costo del diagnóstico. Si el límite de columnas recorta combinaciones, cobertura_diagnosticos lo declara explícitamente. Capa de marcos - regla_evaluacion() acepta proporcion_minima para 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 de granularidades() —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 sfc informan 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 en NA: 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 MULTI compatibles conviven sin hallazgo, mientras que familias distintas y GEOMETRYCOLLECTION se señalan. La validez declara validez_criterio = "planar"; sobre CRS geográficos un fallo planar es sospechoso y no afirma invalidez esférica. - n_dominio_evaluados y n_bbox_evaluados hacen públicos los universos no vacíos de sus métricas; bbox_alcance declara que la caja usa las coordenadas crudas, incluidas las que estén fuera de dominio. n_validez_evaluados publica por separado el universo de GEOS, incluidas las geometrías vacías. dimension_geometria declara XY, XYZ, XYM o XYZM; Z y M quedan enumeradas en dimensiones_no_evaluadas y generan una fila de cobertura. Para XYM y XYZM, la validez topológica se calcula en XY después de st_zm() y validez_preprocesamiento declara ese paso. - El control de dominio compara también las coordenadas transformadas con la BBOX del á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 sin BBOX produce 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 con dependencia = "sf" en cobertura_diagnosticos. Fechas con meses escritos - detectar_formatos_fecha() reconoce fechas con meses escritos en español (incluye setiembre y set) y en inglés, además de los formatos numéricos existentes. La tabla de nombres es propia y no depende de LC_TIME, y sólo acepta la estructura completa de una fecha o de un mes con año: encontrar marzo dentro 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 en NA con estado granularidad_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 estado granularidad_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 hallazgo casi_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, y max_proporcion_grupo_vocabulario permite 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 a detectar_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 = FALSE lo desactiva. El alcance declara los valores y pares comparados, los recortes y la ausencia de stringdist; 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 normalizar declarado en referencial() (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. - CorrectitudSemFuerte y CorrectitudSemDebil pueden 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, umbral 0.10), sólo se calcula para fallos y declara sus límites o la ausencia de stringdist. 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 - normalizar deja de ser sólo un interruptor lógico: TRUE conserva el caso común con minúsculas, espacios, acentos protegidos y comillas; FALSE desactiva 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 reciben normalizar = 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_usados y el estado exacto dejan explícito el alcance real. - El informe de fusiones no se calcula cuando normalizar = FALSE, porque no hay pasos configurables que evaluar. Cuando perfilar() ya lo calculó, detectar_duplicados_aproximados(perfil = ...) lo reutiliza en lugar de recorrer de nuevo el vocabulario. - proteger acepta grafemas compuestos y el valor predeterminado conserva g̃ 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 por p = 0) y el umbral pasa de 0.12 a 0.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 aproximado un par de textos distintos aunque soundex devuelva distancia cero. - Cierra el motor de reparación de texto: decode_inconsistent_utf8 trabaja 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_a0 de 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-3 a GPL-3; las partes derivadas del diseño de ftfy se atribuyen en LICENSE.note bajo 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 stringdist puede usar en las comparaciones aproximadas y declara el valor efectivo en alcance. - El aviso interactivo del camino LSH identifica nucleos como 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 mediante marco_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 <= fin y monto_bruto <= monto_neto). El hallazgo conserva los conteos y las filas fuera de orden, sugiere formalizar la regla con ReglaIntegridadIntraEntidad y declara en meta$orden_columnas las columnas y pares efectivamente comparados. Expone un filtro opcional de solapamiento intercuartil para tablas anchas; está apagado por omisión (umbral 0) 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 desde data-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 integer64 fuera del rango exacto de double y 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 factor en el perfil y devuelve texto al transformar columnas factor con aplicar(). - 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 NA la 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() y escala(), 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.