20 KiB
Analisis matematico y computacional del modelo de consulta INVIMA
1. Resumen tecnico del modelo real implementado
El sistema vigente implementa una plataforma de consulta sobre datos abiertos del INVIMA publicados en Socrata/datos.gov.co. La fuente se configura en backend/APIinvima.json, con base URL https://www.datos.gov.co/resource y ocho datasets: dispositivos, rs_nso, medicamentos, cum_vigentes, homeopaticos, fitoterapeuticos, suplementos y vacunas.
El flujo real es:
- Extraccion:
fetchInvimaDatasetconstruye URLs SODA con$limit,$offset,$select,$wherey$order, aplica token opcional, timeout, reintentos y backoff. - Normalizacion por dataset:
invimaAdaptersconvierte columnas heterogeneas de cada dataset hacia un esquema comun. En este paso se limpian textos, se parsean fechas y se generasource_uidcon SHA-1. - Persistencia:
persistBatchguarda cada lote en dos estructuras:invima_api_raw, con el JSON crudo, einvima_api_catalog, con campos normalizados. - Clasificacion y enriquecimiento:
normalizeCatalogRecordcalculacategoria_canonica,categoria_slugysearch_text. - Indexacion:
ensureInvimaTablescrea indices por dataset, categoria, estado, registro, expediente, modalidad, grupo, fechas, FTS en espanol y trigramas opcionales. - Consulta:
getInvimaCatalogparsea filtros, ejecutarunCatalogQueryy usato_tsvector/websearch_to_tsqueryoILIKEcomo respaldo. - Presentacion: Angular consulta
/api/invima/catalogo, muestra tabla paginada y permite exportar CSV/XLSX por/api/invima/catalogo/export.
Los archivos historicos Productos.csv, products_comands.py, databasepg.js, comandos.txt y comandos en pgadmin4.txt pertenecen al flujo legacy de tabla productos. El README.md del backend indica que ese flujo fue eliminado del modelo vigente y reemplazado por el catalogo unico invima_api_catalog.
2. Variables principales del modelo
En el codigo, la variable independiente primaria no es solo "dispositivo medico"; es el conjunto de criterios de consulta enviado por el usuario:
c = (q, categorias, datasets, estados, modalidades, grupos, fechaVencDesde, fechaVencHasta, fechaExpDesde, fechaExpHasta, onlyVigentes, limit, offset)
Cuando q representa un producto o dispositivo medico, el producto puede tratarse como variable independiente conceptual:
x = producto
Sin embargo, el buscador real aplica q sobre search_text, registro_sanitario, expediente, producto, categoria_canonica y categoria_linea. Por tanto, q es una variable independiente textual general.
Variables dependientes implementadas en invima_api_catalog:
- Identificacion/fuente:
source_dataset_key,source_dataset_id,source_uid,source_dataset_name. - Clasificacion:
categoria_linea,categoria_canonica,categoria_slug,grupo,modalidad. - Registro/producto:
expediente,registro_sanitario,producto,marca. - Actor regulatorio/comercial:
titular,rol_nombre,rol_tipo,ciudad_titular,pais_titular,fabricante,importador. - Estado/tiempo:
estado_registro,vigencia,fecha_expedicion,fecha_vencimiento. - Campos especificos de medicamentos/tecnologias:
principio_activo,forma_farmaceutica,presentacion_comercial,atc. - Campos tecnicos:
search_text,extra,fetched_at,updated_at.
Campos fundamentales para identificar un registro: source_dataset_key + source_uid como clave primaria, y como identificadores semanticos registro_sanitario, expediente, producto y source_dataset_key. Los campos complementarios son los atributos regulatorios, comerciales, de clasificacion y de vigencia.
3. Dominio formal de las variables
Sea R la relacion invima_api_catalog. Los dominios implementados pueden definirse asi:
D_{dataset} = \{source\_dataset\_key\}
D_{uid} = \{source\_uid\}
D_{producto} = \{producto \mid producto \in R\}
D_{registro} = \{registro\_sanitario \mid registro\_sanitario \in R\}
D_{expediente} = \{expediente \mid expediente \in R\}
D_{titular} = \{titular \mid titular \in R\}
D_{estado} = \{estado\_registro \mid estado\_registro \in R\}
D_{categoria} = \{categoria\_slug, categoria\_canonica, categoria\_linea \mid registros\ de\ R\}
D_{modalidad} = \{modalidad \mid modalidad \in R\}
D_{grupo} = \{grupo \mid grupo \in R\}
D_{fechaExp}, D_{fechaVenc} \subseteq Date
D_{texto} = vocabulario(search\_text)
El dominio de busqueda del sistema es el producto cartesiano restringido por registros existentes e indices de PostgreSQL:
D_c = D_{texto} \times \mathcal{P}(D_{categoria}) \times \mathcal{P}(D_{dataset}) \times \mathcal{P}(D_{estado}) \times \mathcal{P}(D_{modalidad}) \times \mathcal{P}(D_{grupo}) \times D_{fechaExp}^2 \times D_{fechaVenc}^2
4. Relacion entre datos
La relacion principal del modelo puede expresarse como:
R \subseteq D_{dataset} \times D_{uid} \times D_{categoria} \times D_{expediente} \times D_{registro} \times D_{producto} \times D_{titular} \times D_{estado} \times D_{fechaExp} \times D_{fechaVenc} \times D_{modalidad} \times D_{grupo} \times D_{marca} \times D_{principio} \times D_{forma} \times D_{presentacion} \times D_{atc} \times D_{rol} \times D_{ubicacion} \times D_{fabricante} \times D_{importador} \times D_{vigencia} \times D_{extra}
Esta formulacion representa correctamente la tabla invima_api_catalog, siempre que se entienda que algunos dominios son opcionales o nulos segun el dataset. La relacion cruda complementaria es:
R_{raw} \subseteq D_{dataset} \times D_{uid} \times JSONB \times D_{timestamp}
5. Ecuacion de consulta o filtrado
La consulta real combina seleccion, ordenamiento, ranking textual y paginacion:
S = \tau_{orden}\left(\sigma_{P_c}(R)\right)
Q(R,c,limit,offset) = \pi_{limit,offset}(S)
Donde P_c(r) es verdadero si se cumplen las condiciones generadas por buildCatalogWhere: texto libre, categorias, datasets, estados, modalidades, grupos, rangos de fechas y vigencia. Si q tiene al menos tres caracteres se usa:
to\_tsvector(search\_text) \@@ websearch\_to\_tsquery(q)
con respaldo por ILIKE en registro_sanitario, expediente, producto, categoria_canonica y categoria_linea. Esta formulacion se ajusta mejor que una simple seleccion, porque el codigo tambien calcula COUNT(*), ts_rank, ORDER BY, LIMIT y OFFSET.
6. Dependencia entre variable independiente y variables dependientes
Si el criterio independiente es un producto o dispositivo medico x \in D_{producto}, la salida es multivaluada:
g(x) = \{r \in R \mid producto(r) \sim x \lor search\_text(r) \sim x\}
La proyeccion de atributos dependientes es:
Y_x = \pi_{registro\_sanitario, expediente, titular, estado\_registro, categoria\_canonica, modalidad, grupo, fecha\_expedicion, fecha\_vencimiento, vigencia, fabricante, importador, source\_dataset\_name}(g(x))
No es una funcion uno a uno: un producto puede aparecer en varios datasets, registros, roles o titulares, y un registro puede generar varias filas si la fuente distingue roles o actores.
7. Ecuacion de transformacion de datos
El sistema si transforma datos antes de consultarlos. La forma general es:
f_d: R_{crudo,d} \rightarrow R_{catalogo}
Para cada fila cruda r_i de un dataset d:
t_i = I(S(C(M_d(N(L(r_i))))))
Donde:
L: limpieza de texto (cleanText), eliminando vacios y texto"null".N: normalizacion de fechas (parseDateFlexible) y campos comunes.M_d: mapeo especifico por dataset (invimaAdapters).C: clasificacion canonica (normalizeCategory) ycategoria_slug.S: construccion desearch_text.I: integracion/upsert transaccional eninvima_api_raweinvima_api_catalog.
La ecuacion representa el flujo real, con la salvedad de que no todos los datasets llenan todos los atributos.
8. Metricas recomendadas
Medibles con el codigo actual:
- Numero de filas crudas:
rawRows. - Numero de filas normalizadas:
catalogRows. - Filas descargadas, persistidas y paginas por sincronizacion:
fetched,persisted,pages. - Datasets exitosos/fallidos durante sincronizacion.
- Conteo de categorias, datasets con datos, vigentes y vencidos.
- Total de resultados por consulta,
limityoffset. - Duracion aproximada de una sincronizacion por
startedAtyfinishedAtdel job en memoria.
Requieren instrumentacion adicional:
- Tiempo promedio/maximo de respuesta por consulta.
- Historial persistente de consultas y tiempos.
- CPU, memoria, disponibilidad y throughput real del servidor.
- Precision, recall, falsos positivos y falsos negativos, porque requieren conjunto de verdad o evaluacion manual.
- Error absoluto entre registros esperados y obtenidos, porque requiere valor esperado externo.
- Repetibilidad estadistica de tiempos, porque hoy no se guarda serie historica de tiempos.
9. Ecuaciones de metricas
Tiempo promedio de respuesta:
T_{prom} = \frac{1}{n}\sum_{i=1}^{n}T_i
Tiempo maximo:
T_{max} = \max(T_i)
Throughput:
Th = \frac{N_{consultas}}{T_{total}}
Tasa de fallos:
TF = \frac{N_{fallos}}{N_{consultas}} \times 100
Porcentaje de consultas exitosas:
CE = \frac{N_{exitosas}}{N_{consultas}} \times 100
Error absoluto:
EA = |R_{esperado} - R_{obtenido}|
Error porcentual:
EP = \frac{|R_{esperado} - R_{obtenido}|}{R_{esperado}} \times 100
Precision:
Precision = \frac{VP}{VP + FP}
Recall:
Recall = \frac{VP}{VP + FN}
Repetibilidad temporal:
Rep = 1 - \frac{\sigma_T}{\mu_T}
Registros procesados por segundo en sincronizacion:
RPS = \frac{N_{persistidos}}{T_{sync}}
Disponibilidad:
A = \frac{T_{activo}}{T_{total}} \times 100
10. Archivos, funciones y endpoints importantes
- Fuente/configuracion:
backend/APIinvima.json;backend/APIinvima.js(buildInvimaUrl,fetchInvimaDataset). - Adaptacion y transformacion:
backend/invima/adapters.js(cleanText,parseDateFlexible,buildUid,baseRecord,invimaAdapters). - Tablas e indices:
backend/invima/service.js(ensureInvimaTables);backend/comando.sql. - Sincronizacion:
backend/invima/service.js(syncOneDataset,syncInvimaDatasets,persistBatch);backend/invima/sync-job-manager.js. - Busqueda/filtros:
backend/invima/service.js(parseCatalogFilters,buildCatalogWhere,runCatalogQuery,getInvimaCatalog). - Exportacion:
backend/invima/service.js(getInvimaCatalogForExport,getInvimaCatalogForExportChunk,toCsv);backend/src/routes/invima.routes.js. - Endpoints:
GET /api/invima/datasets,GET /api/invima/categorias,GET /api/invima/filtros,GET /api/invima/sync/stats,GET /api/invima/sync/job,POST /api/invima/sync,GET /api/invima/catalogo,GET /api/invima/catalogo/export. - Frontend de consulta:
semillero/src/app/core/services/invima.service.ts;semillero/src/app/features/invima/catalog-page.component.ts. - Frontend de sincronizacion:
semillero/src/app/features/invima/sync-page.component.ts. - Manejo de errores:
backend/src/middlewares/error.middleware.js. - Validaciones basicas:
clampInt,parseListParam,parseCatalogFilters,requireAuth.
No se encontro instrumentacion persistente de logs de consulta, tiempos por endpoint o metricas de CPU/memoria. Los logs existentes son console.warn, console.error y mensajes de arranque.
11. Texto academico en tercera persona
La plataforma implementa un modelo computacional de integracion y consulta de datos abiertos del INVIMA basado en una arquitectura de extraccion, normalizacion, almacenamiento e interrogacion sobre PostgreSQL. Los datos se obtienen desde datasets publicados en la API Socrata de datos.gov.co, configurados mediante identificadores de recurso. Cada conjunto de datos posee una estructura original potencialmente diferente, por lo cual el sistema aplica adaptadores especificos que transforman las filas crudas en un esquema comun de catalogo.
El modelo distingue dos niveles de almacenamiento. El primero conserva la representacion cruda de la fuente en formato JSONB, permitiendo trazabilidad frente al origen. El segundo corresponde a una relacion normalizada denominada invima_api_catalog, en la cual se integran atributos comunes como registro sanitario, expediente, producto, titular, estado del registro, fechas, modalidad, grupo, categoria, fabricante, importador y vigencia. Esta relacion constituye el dominio principal de consulta.
Formalmente, el catalogo puede representarse como una relacion R definida sobre dominios de producto, registro, titular, estado, categoria, fechas y fuente. El proceso de consulta corresponde a una operacion de seleccion parametrizada por criterios introducidos por el usuario. Dichos criterios incluyen texto libre, categorias, datasets, estados, modalidades, grupos, rangos de fechas y vigencia. La seleccion se complementa con ordenamiento, ranking textual y paginacion, por lo que la consulta puede expresarse como Q(R,c,limit,offset)=\pi_{limit,offset}(\tau_{orden}(\sigma_{P_c}(R))).
Cuando el usuario introduce un nombre de producto o dispositivo medico, este actua como variable independiente de busqueda. El resultado no es un valor unico, sino un conjunto de registros relacionados que contienen variables dependientes tales como registro sanitario, expediente, titular, estado, categoria, fechas de expedicion y vencimiento, modalidad, grupo, fabricante, importador, fuente y vigencia. Por tanto, la dependencia puede modelarse como una funcion multivaluada g(x), donde x representa el producto consultado y g(x) retorna el subconjunto de filas del catalogo asociadas al criterio.
La transformacion de datos puede formalizarse como f_d: R_{crudo,d} \rightarrow R_{catalogo}. Para cada fila cruda, el sistema ejecuta limpieza textual, normalizacion de fechas, mapeo de columnas segun el dataset, clasificacion canonica, construccion de texto de busqueda e integracion transaccional en la base de datos. Esta formulacion refleja el comportamiento real del codigo y permite describir la plataforma como un modelo reproducible de consulta de datos abiertos, con trazabilidad desde la fuente original hasta la respuesta presentada al usuario.
Para evaluar el modelo se recomienda medir tiempo de respuesta, throughput, tasa de fallos, cantidad de registros procesados por segundo, cantidad de resultados por consulta, consistencia ante consultas repetidas, precision y recall. El codigo actual permite medir conteos de registros, avance de sincronizacion, datasets fallidos, paginas procesadas, vigentes, vencidos y total de resultados. No obstante, las metricas de rendimiento temporal, consumo de recursos, precision y recall requieren instrumentacion adicional y, en algunos casos, conjuntos de referencia validados.
12. Resultados actuales medidos en la base local
Fecha de medicion: 2026-07-07. Ultima actualizacion registrada en los datos: 2026-07-01 01:05:22 UTC.
Volumen y clasificacion
- Registros procesados y normalizados en
invima_api_catalog: 1 407 694. - Registros crudos preservados en
invima_api_raw: 1 407 694. - Datasets con datos: 8.
- Categorias canonicas por
categoria_slug: 21. - Etiquetas de categoria visibles por
categoria_canonica: 28. - Registros vigentes: 751 208.
- Registros vencidos: 632 435.
- Estados distintos: 48.
- Modalidades distintas: 48.
- Grupos regulatorios distintos: 16.
Distribucion por dataset
| Dataset | Registros |
|---|---|
rs_nso |
1 085 799 |
dispositivos |
189 604 |
cum_vigentes |
66 501 |
medicamentos |
54 047 |
suplementos |
6 091 |
homeopaticos |
3 477 |
fitoterapeuticos |
2 127 |
vacunas |
48 |
Dispositivos y equipos medicos
Para el dataset dispositivos, el sistema contiene 189 604 registros consultables. El campo grupo organiza esos registros en cuatro grupos regulatorios:
| Grupo | Registros |
|---|---|
| MEDICO QUIRURGICOS | 91 826 |
| REACTIVO DIAGNOSTICO | 68 529 |
| REACTIVOS IN VITRO | 29 219 |
| ODONTOLOGICOS | 30 |
Estados principales en dispositivos:
| Estado | Registros |
|---|---|
| Vigente | 125 117 |
| Vencido | 47 482 |
| Cancelado | 5 926 |
| Perdida Fuerza Ejec | 4 530 |
| En Estudio | 2 596 |
Demora de consultas SQL
Las demoras se midieron con EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) sobre PostgreSQL local. Los tiempos incluyen planeacion y ejecucion de SQL, no latencia HTTP ni renderizado Angular.
| Consulta SQL representativa | Planeacion | Ejecucion | Total SQL |
|---|---|---|---|
COUNT(*) total de invima_api_catalog |
8.20 ms | 187.55 ms | 195.75 ms |
Catalogo general paginado, LIMIT 25 |
52.88 ms | 6 160.19 ms | 6 213.06 ms |
Conteo de dispositivos |
0.12 ms | 149.81 ms | 149.94 ms |
Conteo de dispositivos vigentes |
6.41 ms | 287.12 ms | 293.53 ms |
Filtro dispositivos vigentes, LIMIT 25 |
1.66 ms | 375.79 ms | 377.45 ms |
Busqueda textual real del backend para marcapasos, LIMIT 25 |
82.85 ms | 40 427.49 ms | 40 510.33 ms |
Conteo de busqueda textual real para marcapasos |
0.27 ms | 37 825.12 ms | 37 825.38 ms |
Como runCatalogQuery ejecuta primero COUNT(*) y despues la consulta paginada, una busqueda textual como marcapasos puede tardar aproximadamente 78.34 s solo en SQL si se suman conteo y pagina de resultados. En contraste, una consulta filtrada por dataset y vigencia en dispositivos tarda aproximadamente 0.67 s en SQL al sumar conteo y pagina.
Tambien se midio la busqueda FTS pura, sin los OR ILIKE adicionales del backend:
| Consulta FTS pura | Resultados | Tiempo observado |
|---|---|---|
marcapasos |
420 | 92.98 ms |
vacuna |
612 | 7.27 ms |
Esto muestra que el indice FTS funciona de forma eficiente, pero la condicion real del backend combina FTS con varios ILIKE, lo que puede producir exploraciones mas costosas.
SQL base usado para los conteos
SELECT
COUNT(*)::bigint AS catalog_rows,
COUNT(DISTINCT source_dataset_key)::int AS datasets_with_data,
COUNT(DISTINCT categoria_slug)::int AS categories,
COUNT(*) FILTER (
WHERE (COALESCE(estado_registro, '') ILIKE '%vigente%'
OR COALESCE(vigencia, '') ILIKE '%vigente%')
AND COALESCE(estado_registro, '') NOT ILIKE '%no vigente%'
AND COALESCE(vigencia, '') NOT ILIKE '%no vigente%'
)::bigint AS vigentes,
COUNT(*) FILTER (
WHERE COALESCE(estado_registro, '') ILIKE '%vencido%'
OR COALESCE(vigencia, '') ILIKE '%vencido%'
)::bigint AS vencidos,
MAX(updated_at) AS max_updated_at
FROM invima_api_catalog;
SELECT grupo, COUNT(*)::bigint AS total
FROM invima_api_catalog
WHERE source_dataset_key = 'dispositivos'
AND grupo IS NOT NULL
AND grupo <> ''
GROUP BY grupo
ORDER BY total DESC;
Parrafo de resultados actualizado
Como resultado de la medicion actual, el modelo computacional consolido 1 407 694 registros procesados y normalizados en la tabla invima_api_catalog, manteniendo igual numero de registros crudos en invima_api_raw para trazabilidad con la fuente original. Para el conjunto de dispositivos medicos y otras tecnologias, integro 189 604 registros consultables, distribuidos en cuatro grupos regulatorios principales: medico quirurgicos, reactivo diagnostico, reactivos in vitro y odontologicos. En el catalogo general, el sistema clasifico 751 208 registros vigentes y 632 435 registros vencidos, organizados en 21 categorias canonicas y ocho datasets oficiales. Las pruebas SQL mostraron tiempos de consulta de 195.75 ms para el conteo total del catalogo, 377.45 ms para recuperar una pagina de dispositivos vigentes y 40.51 s para una busqueda textual completa con las condiciones reales del backend. Estos resultados evidencian la capacidad del modelo para estructurar grandes volumenes de datos regulatorios y tambien muestran que la busqueda textual combinada con condiciones ILIKE requiere optimizacion adicional para mejorar el rendimiento.