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:
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$<columna>$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.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.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.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:
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.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
<sin_configuracion>, 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()."-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.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.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.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):
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.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:
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).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 pa<U+00ED>s
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:
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:
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 ...:
... 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().... 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.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.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:
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.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<U+00ED>"), 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.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.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.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").LC_NUMERIC en coma, detectar_relaciones() separa los extremos de un
rango con punto y coma, como con OutDec: decia [0,125, 0,75].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.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 m<U+00E9>trica(s).?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.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.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.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).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):
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. | <SQL> '[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. | <SQL> '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.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:
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:
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:
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.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:
.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.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."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.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 = <cedula>), la llamada y la formula con ese atributo, el comentario
# titular <cedula> 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."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:
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.typeof() los valores guardados como texto o blob, el conteo va al motivo, y
la corroboracion solo publica la causa medida cuando lo hay.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.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.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.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:
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.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).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.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 num<U+00E9>rico. Los errores del
paquete se arman ahora como una condicion con el texto tal cual.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.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.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:
., ,, :, /, 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.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."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."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.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:
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 a<cd>b 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 <cd>, 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.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).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().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.latin1 salian como texto descripci<f3>n bajo
C. paste() traduce al nativo lo marcado latin1, y bajo C lo nativo
es ASCII: detectar_claves()$columnas publicaba fecha + descripci<f3>n,
detectar_duplicados_aproximados() ponia descripci<f3>n 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.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.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$descripci<f3>n[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 Tel<e9>fono 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:
vctrs_rcrd -o un vctrs_vctr sin conversion declarada- hacia abortar
22 de las 29 llamadas del catalogo ("Can't convert x <vctrs_rcrd> to
tipo_compuesto_no_analizado, con su clase en el hallazgo. Las otras
columnas publican las mismas cifras que sin ella.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().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.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.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.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.bloqueada: salia lista con un motivo que decia que no era ejecutable.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.?perfilar_dbi.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.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.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:
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.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.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.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.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."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.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.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.[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.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.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.$ 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.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.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.?normalizacion), y un texto legitimo con los mismos
bytes que un mojibake se acusa y se reescribe si se aplica la reparacion
(?planificar_limpieza).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.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.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.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().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:
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.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.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."+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.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.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.(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].juan.perez<U+207D>at<U+207E>empresa.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.?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:
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."", "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.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 )().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.POINT EMPTY
en una coleccion o en un MULTIPOINT, que sf escribe como NaN NaN- ya no
se cuenta como coordenada fuera de dominio.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.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.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".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:
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.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.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.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.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.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.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:
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.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:
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:
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.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:
>= 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.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.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():
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:
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.a + b ~= total se perdia
porque la basura de afuera bajaba el cumplimiento a 0,83. Se usa la misma
mascara que el resumen.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.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.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:
.ausente_<columna> 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.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.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.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.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.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.Los validadores, los tipos y las fechas:
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).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).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.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.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.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.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:
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-.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.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 <U+XXXX> y la nota de escapes-.Duplicados aproximados:
\ 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.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." ", " " 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().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.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().pares_omitidos_medicion.Estimaciones externas:
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.medicion: su clase era solo medicion_calidad, y
historico_calidad() la rechazaba. Ahora es c("medicion_calidad", "medicion", "data.frame").no_numerica, que manda a buscar un texto mal escrito.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:
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_<nombre> y .outlier_<nombre>. 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.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.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.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:
[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".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.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 %).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:
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.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".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.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.comparar_evaluaciones() decia "no miden la misma tabla". El motivo vive
ahora en el lector unico del par, y los dos lo publican igual.cobertura_diagnosticos se quedaba en la
consola, y un plan mandado solo se leia como "lo demas esta bien".max_filas por omision, la de
una medicion de 120 celdas quedaba afuera y el informe solo decia "se muestran 100 de
121 filas".medidos_fuera_del_marco- y print() los nombraba; el informe no decia cuales.ctl\x01x se
veia ctlx-, mientras la evidencia del mismo informe los escribia <U+0001>. 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.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.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.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..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.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.data.table conserva su clave.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:
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.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.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.reportar(), mientras con el
acento U+00B4 se tapaba. El pliegue de comparacion borra ahora U+02C6 a
U+02D1.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.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.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.data-raw/medir_tapado_de_mas.R.El historico y la deriva:
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
<sin_configuracion>: 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.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.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.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.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.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.Claves, relaciones y el referencial:
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.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.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.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.cobertura_pares como sin conclusion,
con la mayor cobertura medida y como resolverlo, y meta$pares_sin_conclusion
los cuenta.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.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):
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.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.agregada:ratio:NoNulo y la cobertura [email protected], 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.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.organizacion y
conjuntoColecciones abortaban cuando la lista renombraba sus colecciones; y
los pesos se aceptan con el nombre declarado o con el del objeto.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.Lo que afirma el perfil:
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.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.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.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.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.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.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.[email protected] con una columna juan_perez- se busca entero
antes de apartar el nombre: con otra caja ([email protected]) 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.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.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).-, ;, _,
| 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.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.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).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:
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.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
([email protected]), y agregar() nombra destino, funcion, metrica y
objeto ([email protected]).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:
perfilar_dbi() tapa una cita con sus comillas de CSV.?perfilar lista lo que queda.Una refutacion de medir(), evaluar(), el historico y la deriva encontro doce
defectos.
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.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.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.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:
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.2901-12-12 no es una fecha-, y una fecha con hora protegida se reconoce
escrita en otro formato.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.?perfilar, con su reproductor ampliado.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.Una refutacion del informe de reportar() encontro siete defectos.
[, subset() y rbind()
-que la perdian o se quedaban con la del primer objeto- y al historico.fread(encoding = "UTF-8") sobre un CSV latin1-: lo muestra con sus bytes.titulo ya no se escribe como <br> dentro de <title>.Y una refutacion de la proteccion de datos personales encontro catorce, cinco de ellos abiertos por lo que la ronda 20 aflojo.
?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.maximo de 54321986-, y cuando el piso tapa
un extremo se tapan los campos de la secuencia entera que lo reconstruian.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.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.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.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.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.proteger_datos_personales = FALSE no protege.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:
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.AAAAMMDD) o en ISO a medianoche no entra en el piso; y la unidad
de una columna es vocabulario.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.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.
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.data-raw/mapa_pliegue_comparar.R), que reemplaza al de la ronda
18-B.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.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.no_medible, con aviso. Se publicaba sin_valores con «aportar
valores no nulos», que ?medir define como un universo sin valores.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.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.data-raw/mapa_diacriticos.R) y el pliegue de caja
del paquete, que no depende del locale, la hacen por caracteres.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-.perfilar_dbi(): corrian fuera de toda etapa, y sobre
800.000 filas eran tres cuartos del tiempo sin atribuir.[email protected] 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.juan.perez se
publicaba frente a «Juan Perez» con tilde: las agujas conservaban la letra
acentuada. Ahora se comparan transliteradas, sin depender del locale.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.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.FALSE sin declarar nada. Ahora quedan en NA, con su fila en la cobertura,
igual que cuando falta bit64.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.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.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.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".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.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.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.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).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.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.%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.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.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.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.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."-12345672" era una cedula valida
aunque la documentacion promete no quitar el signo para hacerlo pasar.n_codificacion_rota, y el
perfil publicaba n_codificacion_no_se_pudo = 1 sin hallazgo ni accion.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.validar_url().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.?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.id_medicion y una fecha. Dos corridas con el mismo id y
fechas distintas se mezclaban en agregar() y en el tablero sin aviso.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.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.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.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.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.+ 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.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.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.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() 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.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.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.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.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.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.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.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.id_medicion ya no publica «0 corrida(s)».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.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.contrato_incompleto, porque no dice lo mismo: el
contrato esta completo y son los datos los que no admiten la metrica.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.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."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.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.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.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.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.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.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.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».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.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.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.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.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.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.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.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.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.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.(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.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
-[email protected]- 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.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.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.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.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.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.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.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.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.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.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.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.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.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.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."(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.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.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".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.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.checking tests no se movio con la optimizacion, asi que el costo de la funcion no
era lo que lo explicaba.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.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.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.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.configuracion_modelo, de modo que un historico anterior puede
mostrar un cambio de modelo en el limite entre las dos versiones.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.
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.
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.
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.
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.
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.
?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.
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 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.
.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.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.
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.
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.
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 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 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.
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.
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".
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.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.
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.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 %.
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".
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.
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
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.
.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.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.\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().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..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.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.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.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.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.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.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.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—.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.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.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.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.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.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.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.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.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.max_largo_valor = Inf, documentado como «sin tope», abortaba el camino
LSH con valores de 9.999 bytes o más.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.[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.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.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.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.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..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.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.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.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.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.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.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.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.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.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.faltante_misma_clase ya no aparece sobre columnas de clases
distintas: dice qué tipo cambió.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().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.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.delta = 0.
Dos corridas idénticas ahora dicen que el resultado se mantuvo.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.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.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.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.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.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.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.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.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—.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.
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.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.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.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.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.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.%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".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.%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."2023-05 14:30" no existe.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.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.9999 y veinte 9998— la
media declarando ambos códigos pasa de 537,9 a 39,96, que es la verdad.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.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ó.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.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.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.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.+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.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ó.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.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.[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.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.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.fecha_fallecimiento por nombre, se protegen sus momentos y se
informa fecha_fallecimiento_fuera_rango sin publicar las fechas observadas.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.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.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—.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.tolower() dependiente del locale.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.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é.
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.\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.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.
dimension_geometria.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.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.sf. Un GDAL real de win-builder reventaba —violación
de segmento, no error— con ese trunco, y tryCatch no atrapa eso.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.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.no_disponible y motivo explícito.n_distintos; aplica politica_costo = "por_cardinalidad", registra
dbfetch_bloques como estado acotado y audita filas, orden y pasadas.pg_stat_statements por
queryid, y los avisos de estimacion declaran el origen de su denominador
aun cuando validos no fue solicitado.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.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.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.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.NA, NaN e infinitos; un resumen SQL que no
pueda leer +/-Inf puede divergir por la representación del controlador.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.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.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.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.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.estado_tipo_inferido como confirmado,
candidato o NA; el resumen impreso anota sólo los tipos candidatos.no_disponible sin degradarse silenciosamente..muestrear_vector()
para 1, 2, 7 y 31 bloques.orden, snapshot, topes y truncamiento; la
integración DBI y su ORDER BY queda reservada para la Etapa 4.type = 7 y median() bajo el filtro
explícito is.finite.no_disponible con motivo y resolución sugerida.iniciar/absorber/fusionar/finalizar,
con medición de bytes residentes y sobre uniforme de resultado.NA, NaN, +0/-0 e integer64.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.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".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.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.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.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.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.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.meta$costo_moda y
meta$costo_mediana, junto con el ya existente meta$costo_distintos.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.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.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.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.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.dbFetch(n), sin traer todo para recortarlo en R.
plan_perfilado_dbi() anticipa el recorte de celdas y la sonda de bytes.cobertura_diagnosticos con celdas o bytes
observados, umbral, motivo y cuál tope mandó. Con ambos topes en Inf no se
declara un recorte.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.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.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.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.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.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.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.flujo-guiado, que recorre una propuesta editable desde
perfilar() hasta evaluar() y muestra el orden de la API.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.perfilar() y ambos README explican la separación y
los conteos de cada hallazgo.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.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.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.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.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().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.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.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í.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.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.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().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.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.no_declarada: «no se pudo preguntar» disfrazado de «no
declara».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.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.largo_maximo publica ese largo. Sin normalización que expanda y con una sola
columna, la conducta no cambia.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ó.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.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.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.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.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.benchmark/medir_topes_valores_ancho.R para reproducir la medicion
de discriminacion y el barrido de tablas anchas.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.
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.
SUM(COUNT(*)) OVER () AS n_validos_guard junto a la frecuencia y valida frecuencia_moda <= n_validos
dentro de esa misma sentencia.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.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.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.
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.resumen_tabla$sql. La ruta de
PERCENTILE_CONT(...) WITHIN GROUP no cambia.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.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 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.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.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 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.
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».
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.
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í.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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
La instrumentación de DBI publica ahora cpu_ms junto a duracion_ms en
resumen_tabla$sql y en resumen_tabla$tiempos. Se calcula con
proc.time() como user.self + sys.self: cerca de cero distingue espera
del trabajo del proceso cliente, y cerca de uno al dividir CPU por tiempo
transcurrido indica que el cliente está trabajando. Cero es una medición válida;
NA queda reservado para lo que no se pudo medir, y
instrumentar = FALSE deja el campo en NA como los demás. En un
microbenchmark de un millón de consultas simuladas, dos lecturas de
proc.time() costaron 1,122 microsegundos por consulta; dos de Sys.time()
costaron 1,778 microsegundos. En la tabla ancha de 158 columnas, el agregado
no duplicó el costo de instrumentar: cinco corridas con reloj solamente tuvieron
mediana de 1,093 s y cinco con reloj más CPU de 0,962 s, una diferencia dominada
por la variación de SQLite. El número reproducible que se publica es el costo
directo de 1,122 microsegundos por consulta.
Moda y mediana se pueden controlar con politica_costo. El valor por omisión
es "todas", que conserva el perfil anterior; "por_cardinalidad"
hace primero los conteos baratos de valores válidos y distintos y decide luego
por columna. Si n_distintos / n_validos alcanza umbral_cardinalidad, omite
la moda de esa columna. La omisión no desaparece ni se vuelve NA silencioso:
queda omitido_por_costo con el motivo, la proporción observada y la forma de
pedirla igual (politica_costo = "todas") o mover el umbral.
Cuando se escribió esta entrada el umbral valía
0.95y omitía moda y mediana. Las dos cosas cambiaron al medirlas —el umbral es0.5y la mediana ya no se omite por proporción, ver la entrada de arriba—, y el texto se corrigió acá para que no queden dos descripciones del mismo argumento diciendo cosas distintas en el mismo archivo.
La política hace explícito el plan en dos etapas. En una tabla reproducible de
158 columnas, 80 numéricas, 200 filas y 60 columnas con cardinalidad al menos
0,95, politica_costo = "todas" emitió 260 consultas y la política
selectiva 140: se ahorraron 120 consultas, no tiempo. Se omitieron 60 modas
y 60 medianas. El valor por omisión no cambia: esos 60 casos muestran el ahorro
posible, pero no autorizan al paquete a elegir qué métrica sacrificar.
Cuando la sonda reconoce un motor con PERCENTILE_CONT(...) WITHIN GROUP,
las medianas numéricas se consolidan en un SELECT por lote; el camino
actual, una mediana por columna, queda como respaldo si la capacidad no está
disponible o falla la consulta consolidada.
El 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.
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.
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.
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.
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.
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.
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.
.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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.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.
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.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.
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.
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.
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.
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.
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 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 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.
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.
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.
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.
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.
Aparecio al intentar romper el arreglo de arriba. Un codigo 1..284 con 179 valores fuera de los limites de Tukey:
integer, se callaba y quedaba declarado en cobertura;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.
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.
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.
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.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.
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.rawToChar(), porque un
literal en el fuente lo marca el parser de R y el caso no se ejercita.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.
test-ronda107.R.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.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.
a.m. y p.m. estan en casi todos
los valores; Delayed esta en uno.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.n_pares_descartados_formato.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.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.
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.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 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.
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.
Los dos eran nuestros y de esta semana, y los dos son la misma forma de error: medir una cosa y publicarla como otra.
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.max_trabajo recorta a 1.000 formas. La tasa real es unos
70.000 pares/seg, no "70.000 a 270.000".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.
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.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.
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.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.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.
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.
DESCRIPTION declaraba R (>= 3.6.0), y era una promesa que el paquete no
cumple. Pasa a R (>= 4.1.0).
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.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).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.Las tres tenian la misma forma, y solo se ven en un entorno donde el paquete opcional de verdad no esta.
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.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.
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.
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".-1, -4, -10) y las
variantes de nombre (LONG VARCHAR, SQL_LONGVARCHAR, WLONGVARCHAR) que
tampoco veia. Pero la correccion ya no cuelga de el.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.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.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.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.
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.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.benchmark/medir_costo_texto.R, seccion 5, para que el umbral no sea un
numero elegido a dedo.supuesto_costo lo
declara en vez de prometer segundos.max_trabajo_vocabulario—, que las palancas del
motor no tocan.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.
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.
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.
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—.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.CAST distinto por
dialecto habria sido otra superficie que mantener y probar contra cada motor.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.
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.
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.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.muestra_motor la selección única queda además en
un spool cliente y las pasadas reutilizan esa relación.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.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.
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.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.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.meta$materializacion en muestra_motor; en los modos que miden sobre la
tabla entera no hay un spool que advertir.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.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.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.esperado, y las
pruebas de este camino corren con respuestas DBI simuladas, que es lo que se
declara.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.
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.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.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.cobertura_de_partes y marca completo = FALSE.agregar() escribia el
nombre del objeto y el nivel de arriba comparaba contra el declarado. Los dos
identifican a la misma parte.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.
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.agregar() se niega y explica como declararla, que es
distinto de inventar una frontera que nadie nombro.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.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.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.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.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.perfilar() la acepta. La
conversion queda declarada en meta$entrada_convertida.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.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.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.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.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."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.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.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.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.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.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.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.NA, y datos[NA, ] devuelve una fila
entera de NA en vez de ninguna. relaciones_coleccion() filtra con which().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.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.top por su cuenta.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.
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.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.SELECT *, y si la
lectura conjunta falla sondean columna por columna para descartar solo la que
el motor rechaza.stop() de la via DBI tienen clase de condicion propia, asi que un fallo
se puede atrapar y el resumen rescatar.calculado y valor
vacio, que era peor que declararlas no disponibles.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.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.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.NA con cobertura_diagnosticos vacia, y
cobertura_analisis() llegaba a afirmar que la geometria no aplicaba sobre
datos que si eran geometricos.NA de la validez dejan de borrar el conteo y los
indices de todas las demas.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.cobertura_diagnosticos. Un universo recortado sin
constancia seria el mismo defecto al reves.n_aplicabilidad_indeterminada, porque no saber no
es lo mismo que no corresponder.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.NoNulo acepta aplicable con el mismo criterio, para que el
universo declarado llegue al tablero y no solo al hallazgo.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.vacio-por-diseno, que documenta el supuesto tabular del
paquete y las seis formas de tabla donde no vale..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.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.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.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.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.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.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.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.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.NA en
n_afectados y deja el motivo en cobertura_diagnosticos; una matriz no
analizada enumera todas sus filas en la trazabilidad.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.
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.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.cobertura_diagnosticos informa cuántos grupos quedaron bajo
el piso y cómo bajarlo.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.momento en que
se midió y meta$snapshot declara que no lo hubo.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.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.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.0,30 y un tamaño de muestra de
0,30 no se leen igual. estadisticos_estimacion() publica el catálogo.lupa lo hubiera calculado. Los estadísticos que la
tabla no traiga no se rellenan con ceros: se declaran ausentes.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.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.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.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.... 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í.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.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.claves_protegidas declara cuáles se enmascararon.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.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.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.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.analizar(), no en perfilar()— y por qué esa separación
es deliberada.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.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.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.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.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_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.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().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_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.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.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.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.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.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.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.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.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.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.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.sf, el perfil no inventa ceros ni hallazgos: registra una fila con
dependencia = "sf" en cobertura_diagnosticos.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.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.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.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.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.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.n_usados y el estado exacto
dejan explícito el alcance real.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.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.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.
stringdist puede usar en las
comparaciones aproximadas y declara el valor efectivo en alcance.nucleos como la perilla que
puede acortar la etapa de comparación, sin prometer una ganancia fija.propiedades_metrica() para consultar la configuración admitida sin
inspeccionar closures.marco_iso25012() como adaptación opcional y explícita de las
quince características de ISO/IEC 25012:2008.cobertura_analisis().marco_calidad(), validar
modelos contra ellas y calcular cobertura con AGESIC sólo como valor de
fábrica mediante marco_agesic().numeric() los desactiva explícitamente.documento_identidad.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.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.
GradoOportunidad*.vigencia() y escala(), y una tabla de
cobertura que distingue lo medido, no declarado, no aplicable y fuera de
alcance.