El problema con el análisis de datos sanitarios
Las organizaciones sanitarias disponen de datos valiosos, pero compartirlos es un campo minado. Un hospital quiere estudiar las tasas de lesión renal aguda en su población de UCI. Un equipo de investigación externo tiene la experiencia estadística necesaria. Los datos existen — cientos de miles de observaciones de laboratorio almacenadas como recursos FHIR en Aidbox. Pero entregar registros de pacientes sin procesar viola HIPAA.
La solución habitual es un pipeline ETL a medida: exportar datos, ejecutar un script para eliminar los identificadores, confiar en que nada se escape, y cargar los datos depurados en otro lugar. Esto es lento, propenso a errores y difícil de auditar. Cada nueva pregunta de investigación implica otro pipeline.
¿Y si la seudonimización ocurriera dentro de la base de datos, declarada junto a la vista que da forma a los datos?
La seudonimización como característica de ViewDefinition
Aidbox ahora admite la seudonimización por columna en ViewDefinitions. Se anotan las columnas con una extensión FHIR que especifica qué transformación aplicar. El compilador SQL envuelve cada expresión de columna con una función de PostgreSQL — los datos sensibles se transforman antes de llegar a la salida.
{
"resourceType": "ViewDefinition",
"name": "deident_observations",
"status": "active",
"resource": "Observation",
"select": [{
"column": [
{
"name": "patient_id",
"path": "subject.getReferenceKey(Patient)",
"extension": [{
"url": "http://health-samurai.io/fhir/core/StructureDefinition/de-identification",
"extension": [
{"url": "method", "valueCode": "cryptoHash"},
{"url": "cryptoHashKey", "valueString": "my-secret-key"}
]
}]
},
{
"name": "status",
"path": "status"
}
]
}]
}
La columna patient_id lleva una extensión de seudonimización. Generará un hash HMAC-SHA256 en lugar de la referencia real del paciente — determinista (el mismo paciente siempre obtiene el mismo hash) e irreversible (no es posible recuperar el valor original a partir del hash). La columna status no tiene extensión y se transfiere sin cambios.
El constructor de ViewDefinition en la interfaz de Aidbox hace esto visual — haga clic en el icono de escudo en cualquier columna para elegir un método y configurar los parámetros:

Métodos disponibles
| Método | Qué hace | Parámetros clave |
|---|---|---|
| redact | Reemplaza con NULL | — |
| cryptoHash | Hash HMAC-SHA256 (determinista, unidireccional) | cryptoHashKey |
| dateshift | Desplaza fechas ±1–50 días por recurso | dateShiftKey |
| birthDateSafeHarbor | Desplazamiento de fecha + redacción automática si edad >89 (solo Patient.birthDate) | dateShiftKey |
| encrypt | AES-128-CBC, salida en base64 (reversible con clave) | encryptKey |
| substitute | Reemplaza con una cadena fija | replaceWith |
| perturb | Añade ruido aleatorio a valores numéricos | span, rangeType, roundTo |
| custom_function | Llama a su propia función de PostgreSQL | custom_function, custom_arg (opcional) |
Dos métodos son especialmente relevantes para el patrón que vamos a explorar:
cryptoHash produce la misma salida para la misma entrada — lo que significa que puede usarlo para unir tablas. Aplique el hash al ID del paciente en dos ViewDefinitions distintas con la misma clave, y los hashes coincidirán. Esta es la base para la reidentificación.
dateshift desplaza todas las fechas dentro del mismo recurso con el mismo desplazamiento, preservando las relaciones temporales. Una diferencia de 3 días entre dos eventos sigue siendo de 3 días. El desplazamiento se deriva de HMAC(dateShiftKey, resource.id), por lo que es determinista por recurso pero impredecible sin la clave.
Ejemplo práctico: subcontratar la investigación de lesión renal aguda
Recorramos un escenario realista usando datos MIMIC-IV cargados en Aidbox — 100 pacientes de UCI, más de 800.000 observaciones.
El objetivo: Un hospital quiere que un equipo de investigación externo analice las tasas de lesión renal aguda (AKI) en su población de UCI. Los investigadores necesitan resultados de creatinina en laboratorio, pero no deben ver los identificadores reales de los pacientes ni las fechas exactas.
El enfoque: Crear dos tablas materializadas — un conjunto de datos seudonimizado para los investigadores, y una tabla de mapeo interna que conecta los IDs hasheados con los pacientes reales.
Diseño de la vista seudonimizada
Comenzamos con una ViewDefinition sobre el recurso Observation, filtrada a resultados de laboratorio de creatinina. Cuatro columnas, cada una con una estrategia de seudonimización diferente:
ID del paciente — la columna que más protección necesita. Aplicamos cryptoHash para convertir el UUID real en una cadena hexadecimal HMAC-SHA256 irreversible:
// ---8<--- snip
{
"name": "patient_id",
"path": "subject.getReferenceKey(Patient)",
"extension": [{
"url": "..de-identification",
"extension": [
{"url": "method", "valueCode": "cryptoHash"},
{"url": "cryptoHashKey", "valueString": "aki-study-2026"}
]
}]
}
// ---8<--- snap
Valor de creatinina y unidad — los datos clínicos que el investigador realmente necesita. Sin seudonimización — los valores de laboratorio no son identificadores:
// ---8<--- snip
{
"name": "creatinine",
"path": "value.ofType(Quantity).value",
"type": "decimal"
},
{
"name": "unit",
"path": "value.ofType(Quantity).unit"
}
// ---8<--- snap
Fecha efectiva — desplazada por un desplazamiento determinista por recurso usando dateshift. Una diferencia de 3 días entre dos análisis sigue siendo de 3 días, pero la fecha real del calendario queda oculta:
// ---8<--- snip
{
"name": "effective_date",
"path": "effective.ofType(dateTime)",
"extension": [{
"url": "..de-identification",
"extension": [
{"url": "method", "valueCode": "dateshift"},
{"url": "dateShiftKey", "valueString": "aki-study-2026"}
]
}]
}
// ---8<--- snap
La tabla de mapeo
La segunda ViewDefinition es para la reidentificación — permanece de uso interno. Dos columnas sobre el recurso Patient, ambas apuntando a id: una en texto plano y otra hasheada con la misma clave que la anterior. Como cryptoHash es determinista, los IDs hasheados coincidirán entre ambas tablas.
Construcción en la interfaz
El constructor de ViewDefinition hace esto visual. Haga clic en el icono de escudo en cualquier columna para elegir un método de seudonimización y configurar sus parámetros:

Una vez guardado, materialice como tabla:

Las ViewDefinitions con extensiones de seudonimización solo pueden materializarse como table, no como view ni materialized-view. PostgreSQL almacena la definición SQL completa de las vistas en los catálogos del sistema (pg_views), lo que expondría sus claves criptográficas a cualquier persona con acceso al catálogo. Las tablas almacenan únicamente los datos transformados.
Lo que ve el investigador
El investigador consulta la tabla seudonimizada. Sin IDs de paciente reales, sin fechas reales:
SELECT * FROM sof.deident_creatinine LIMIT 5;
patient_id | creatinine | unit | effective_date |
|---|---|---|---|
| db1d98d58...f1508c7b439aba | 1.1 | mg/dL | 2113-01-17T09:45:00-05:00 |
| db1d98d58...f1508c7b439aba | 1.4 | mg/dL | 2116-03-05T16:16:00-05:00 |
| f852c0e57...ca9c6e3db462a9 | 0.8 | mg/dL | 2153-03-08T02:17:00-04:00 |
| 59e60d080...01688175f47b16 | 1.7 | mg/dL | 2147-09-09T15:20:00-04:00 |
| 6283390bd...738cd02dac4c08 | 0.8 | mg/dL | 2140-10-03T15:20:00-04:00 |
El patient_id es una cadena hexadecimal — el investigador puede agrupar por ella, contar pacientes distintos y seguir tendencias a lo largo del tiempo. Pero no puede revertirla para encontrar al paciente real.
Cálculo de la métrica
La lesión renal aguda se identifica habitualmente por la elevación de creatinina. Un criterio de cribado simplificado: creatinina superior a 1,5 mg/dL indica posible daño renal. El investigador ejecuta:
SELECT
CASE
WHEN creatinine <= 1.1 THEN 'Normal (≤1.1)'
WHEN creatinine <= 1.5 THEN 'Elevated (1.1–1.5)'
WHEN creatinine <= 3.0 THEN 'High (1.5–3.0)'
ELSE 'Critical (>3.0)'
END AS risk_category,
count(*) AS observation_count,
count(DISTINCT patient_id) AS patient_count,
round(avg(creatinine)::numeric, 2) AS avg_creatinine
FROM sof.deident_creatinine
GROUP BY 1
ORDER BY 1;
risk_category | observation_count | patient_count | avg_creatinine |
|---|---|---|---|
| Critical (>3.0) | 307 | 19 | 5.08 |
| Elevated (1.1–1.5) | 393 | 40 | 1.33 |
| High (1.5–3.0) | 575 | 30 | 2.11 |
| Normal (≤1.1) | 1728 | 84 | 0.77 |
19 pacientes tuvieron lecturas de creatinina superiores a 3,0 mg/dL — un umbral crítico que sugiere lesión renal grave. El investigador identifica a estos pacientes por sus IDs hasheados y envía los resultados al hospital.
Reidentificación para seguimiento clínico
El hospital une los resultados de la investigación con la tabla de mapeo interna — haciendo coincidir los IDs hasheados con los UUIDs reales de los pacientes:
SELECT
m.real_id,
max(o.creatinine) AS peak_creatinine,
count(*) AS high_readings
FROM sof.deident_creatinine o
JOIN sof.patient_id_map m ON o.patient_id = m.hashed_id
WHERE o.creatinine > 3.0
GROUP BY m.real_id
ORDER BY peak_creatinine DESC
LIMIT 20;
real_id | peak_creatinine | high_readings |
|---|---|---|
| af0e5009-d87d-52a8-ac8a-676e471c41f1 | 15.2 | 9 |
| 8e77dd0b-932d-5790-9ba6-5c6df8434457 | 11 | 80 |
| 1cf9e585-806c-513b-80af-4ca565a28231 | 9.1 | 62 |
| 568cb149-804c-59e8-bdf5-816e8151cd22 | 8.3 | 9 |
| df756e08-6ea8-5d69-b918-67911945f827 | 7.9 | 11 |
| 7ec7078a-0593-5a99-9862-ebbff47fd1c5 | 6.4 | 20 |
| dd2bf984-33c3-5874-8f68-84113327877e | 6.2 | 26 |
| 4f773083-7f4d-5378-b839-c24ca1e15434 | 5.3 | 11 |
| 72d56b49-a7ee-5b9a-a679-25d1c836d3c3 | 5.1 | 3 |
| ... | ... | ... |
| 842680b3-e421-58cc-8050-3b29668b438c | 3.2 | 1 |
Ahora el hospital sabe exactamente qué pacientes necesitan atención clínica — sin haber expuesto nunca sus identidades al equipo de investigación.
El panorama completo
La idea clave: la misma cryptoHashKey lo une todo. El investigador trabaja exclusivamente con identificadores hasheados. El hospital guarda la tabla de mapeo y la clave. La reidentificación ocurre solo de forma interna y solo cuando es necesario.
ViewDefinitions Safe Harbor predefinidas
Escribir reglas de seudonimización para cada tipo de recurso es tedioso. Proporcionamos io.health-samurai.de-identification.r4 — un paquete FHIR disponible a través del registro de artefactos de Aidbox con ViewDefinitions predefinidas para 17 tipos de recursos R4 habituales: Patient, Encounter, Observation, Condition, Claim, ExplanationOfBenefit, AllergyIntolerance, DiagnosticReport, MedicationRequest, MedicationDispense, MedicationAdministration, Immunization, Procedure, Specimen, DocumentReference, Practitioner y Location.
Cada ViewDefinition aplica las reglas Safe Harbor:
- cryptoHash en identificadores y referencias — coherente entre tablas para uniones
- dateshift en fechas clínicas — preserva las relaciones temporales
- birthDateSafeHarbor en Patient.birthDate — desplazamiento de fecha más redacción automática cuando el paciente tiene más de 89 años, según lo exigido por 45 CFR 164.514(b)(2)(i)(C)
- redact en nombres, direcciones y otros identificadores directos
Todos los parámetros de clave criptográfica están en blanco por defecto — configure sus propias claves antes de materializar. Instale el paquete mediante FAR, configure las claves, materialice como tablas y comience a consultar.
Primeros pasos
- Active el modo fhir-schema — configure
fhir.validation.fhir-schema-validation=true(obligatorio desde Aidbox 2604) - Instale
io.health-samurai.de-identification.r4mediante FAR, o escriba sus propias ViewDefinitions - Configure las claves criptográficas en cada ViewDefinition — las claves en blanco serán rechazadas
- Materialice como tablas —
$materializecontype=tableo mediante el constructor de ViewDefinition - Consulte con SQL desde la consola SQL de Aidbox, Grafana o cualquier herramienta de BI
Para la referencia completa de métodos y detalles de parámetros, consulte la documentación de seudonimización. Para probar el escenario completo de esta entrada con un único docker compose up, consulte el ejemplo de seudonimización.




