Cómo Aplanar JSON FHIR en una Tabla Analizable
Hl7 Tools

Cómo Aplanar JSON FHIR en una Tabla Analizable

Por Qué FHIR Se Resiste a la Hoja de Cálculo

FHIR es un árbol. Un Patient lleva un array de nombres y cada nombre lleva a su vez un array de nombres de pila; un Observation anida un código dentro de un CodeableConcept dentro de un array de Codings. En cambio, todas las herramientas con las que realmente se analiza —una hoja de cálculo, una tabla SQL, un dataframe, un cuadro de mando— quieren un rectángulo. Pasar de una cosa a la otra es el trabajo más habitual y menos vistoso de la ingeniería de datos clínicos, y es justo donde se pierde fidelidad sin darse cuenta.

Esta guía trata el problema conceptual más que un script concreto: qué significa exactamente una "fila plana" en FHIR, cómo tratar los índices de array, qué se pierde al elegir una fila por recurso y cómo escoger columnas que respondan a una pregunta real. Para verlo funcionando sobre tus propios datos, el Explorador de Bundles FHIR aplica este modelo en el navegador. Si necesitas antes la base estructural —qué es un Bundle y qué vive dentro de entry— empieza por Qué es un Bundle FHIR.

Rutas Hoja: La Unidad del Aplanado

El modelo mental correcto no son los "campos" sino las rutas hoja. Se recorre el árbol del recurso hasta llegar a un primitivo —una cadena, un número, un booleano— y se anota la ruta con puntos que llevó hasta allí. Un Patient produce hojas como id, gender, birthDate, name.family, name.given, telecom.value, address.city. Un Observation produce status, code.coding.code, code.coding.display, valueQuantity.value, valueQuantity.unit, subject.reference.

Las hojas son la unidad adecuada porque son lo único que cabe en una celda. Un nodo intermedio como Observation.code es un objeto; no hay forma honesta de meterlo en una celda sin serializarlo de nuevo a JSON (lo que anula el propósito) o elegir en silencio uno de sus hijos. Trabajar hoja a hoja obliga a que la decisión sea explícita: decides tú que la columna que quieres es code.coding.code y no code.text.

Algunos subárboles son ruido para el análisis y conviene excluirlos de entrada. meta guarda contabilidad del servidor (versión, última actualización, URLs de perfil). text guarda la narrativa legible como un div XHTML, un párrafo de prosa que reventaría cualquier tabla. Los subárboles de extension son ilimitados y específicos de cada sitio. Descartar meta, text y extension antes de enumerar hojas es lo que mantiene a un Patient en una docena de rutas útiles en lugar de ochenta casi vacías. El Explorador de Bundles excluye precisamente esos tres por defecto.

La Profundidad No Es Tu Amiga

FHIR permite recursos contained: un recurso completo embebido dentro de otro porque no tiene existencia independiente. Eso añade un nivel de anidamiento y, en documentos patológicos, puede añadir varios. Cualquier pasada de aplanado necesita un techo de profundidad para que un fichero extraño no la haga girar indefinidamente; un límite en torno a una docena de niveles queda muy por encima de lo que contienen los datos clínicos reales y aun así termina ante un fichero malformado.

Colapsar los Índices de Array: La Decisión Que Lo Condiciona Todo

Aquí está la bifurcación. Un Patient con dos nombres produce, si se respetan los índices, name[0].family y name[1].family: dos rutas distintas. Con tres nombres, tres. Si conservas los índices en la ruta, tu conjunto de columnas pasa a depender del recurso más verboso del fichero: un paciente inusualmente largo añade columnas vacías para todos los demás, y dos ficheros del mismo tipo de dato dejan de compartir esquema.

Colapsar los índices —tratar name[0].family y name[1].family como la misma ruta, name.family— resuelve eso. Todos los valores que pertenecen al mismo campo conceptual caen en una sola columna, el conjunto de columnas depende únicamente de qué campos están poblados y dos exportaciones del mismo tipo de recurso encajan entre sí. Es el modelo que usa el Explorador de Bundles FHIR: las rutas se escriben sin índices, de modo que todos los valores de un campo repetido comparten una única columna.

El precio es que una columna puede contener ahora más de un valor para un mismo recurso, y hay que decidir qué ocurre entonces. Esa es la siguiente decisión.

¿Una Fila por Recurso o un Valor por Celda?

Solo hay dos respuestas defendibles, y se contraponen.

Modo Join — Una Fila por Recurso

Se mantiene el número de filas igual al número de recursos y los valores repetidos se empaquetan en la celda con un separador, por convención el punto y coma: una paciente con los nombres María y Elena muestra María;Elena en la columna name.given. No se descarta nada, el número de filas es predecible y un COUNT(*) sobre la tabla responde correctamente a "¿cuántos pacientes hay?".

El coste es que las celdas multivaluadas no son analizables directamente. No puedes agrupar por name.given y obtener grupos con sentido, porque María;Elena es un grupo en sí mismo. Todo lo que aguas abajo necesite valores individuales tendrá que partir la celda antes.

Modo Expand — Un Valor por Celda

La alternativa es emitir una fila por repetición. Si la columna seleccionada más ancha de un recurso contiene tres valores, ese recurso genera tres filas; cada fila toma el i-ésimo valor de cada columna multivaluada, y las columnas de valor único se repiten hacia abajo para que cada fila siga siendo autodescriptiva: el id del recurso y la referencia al sujeto aparecen en las tres.

Así cada celda contiene un valor y agrupar, filtrar y cruzar se comportan como espera quien usa una hoja de cálculo. El coste es que el número de filas ya no coincide con el de recursos, de modo que un conteo ingenuo infla la cifra y una suma sobre valores únicos repetidos duplica. Ninguna de las dos cosas es grave mientras sepas en qué modo estás; el fallo aparece al cambiar de modo y olvidarlo.

Una regla práctica: usa el modo join cuando la tabla es un inventario legible o una exportación destinada a un sistema que volverá a parsearla, y el modo expand cuando alimenta una agregación —distribuciones de valores, frecuencias de códigos, recuentos por categoría—. Y cuando el destino es un fichero y no una pantalla, esa misma elección gobierna la exportación; nuestra guía sobre convertir un Bundle FHIR a CSV explica cómo aterrizan ambos modos en un fichero conforme a RFC 4180.

Emparejar por Posición No Es Alinear por Semántica

Una advertencia honesta sobre el modo expand: empareja columnas por posición. Si telecom.value tiene dos entradas y address.city tiene dos, la primera fila junta la primera de cada una y la segunda fila las segundas, pero FHIR no promete en ningún momento que el primer teléfono corresponda a la primera dirección. El emparejamiento posicional es una comodidad de presentación, no un cruce semántico. Cuando la correspondencia importa de verdad, selecciona columnas de una única rama repetida (todo name.*, por ejemplo) en lugar de mezclar ramas, y entonces la alineación sí es real.

Cómo Aplanar JSON FHIR en una Tabla Analizable

Elegir Columnas: Curadas, Derivadas o Todas

Enumerar hojas te da todas las columnas posibles, que casi nunca es lo que quieres. Un Observation realista de un feed de laboratorio puede exponer cuarenta o más rutas distintas, la mayoría pobladas en un puñado de instancias. Tres estrategias cubren casi cualquier necesidad.

  • Presets curados. Para los tipos de recurso que dominan los bundles reales, una lista de columnas escogida a mano responde de inmediato a la pregunta evidente. El Explorador de Bundles incluye nueve: Patient, Observation, Condition, Encounter, MedicationRequest, Procedure, AllergyIntolerance, Immunization y DiagnosticReport. Cada preset se filtra a las rutas realmente presentes en tus datos, de modo que un bundle cuyos pacientes no traen dirección nunca muestra una columna de dirección vacía.
  • Columnas derivadas. En R4 hay alrededor de 150 tipos de recurso y nadie debería mantener presets a mano para todos. Para cualquier tipo no curado —o un tipo curado cuyas rutas del preset están todas ausentes— se ordenan todas las rutas descubiertas por cuántos recursos las pueblan y se toman las doce primeras. La frecuencia es un buen indicador de utilidad: una ruta presente en todos los recursos casi con seguridad forma parte de la identidad del registro; una presente en dos es un caso límite.
  • Todas. Conviene mantener una salida de emergencia que muestre todas las rutas descubiertas. Así se encuentra el campo que el preset no anticipó —un identificador propio del centro, una elección de value[x] poco común— y es la respuesta honesta a "¿la herramienta ha descartado mis datos?".

Sea cual sea la estrategia, las columnas deberían llevar a la vez una etiqueta corta para leer y la ruta completa con puntos para exportar. La etiqueta corta (value para valueQuantity.value) mantiene la tabla legible; la ruta completa es lo que debe ir en la cabecera del CSV, porque es inequívoca y reversible.

El Problema de value[x]

Los elementos de tipo elegible merecen un aviso específico. Observation.value[x] se serializa como valueQuantity, valueString, valueCodeableConcept, valueBoolean y otros según qué haya medido la observación. Aplanados hoja a hoja, son rutas distintas y por tanto columnas distintas, así que un fichero que mezcla resultados numéricos de laboratorio y hallazgos codificados produce una tabla dispersa en la que cada fila rellena solo una de las columnas de valor. Eso es correcto, no un fallo: los datos subyacentes son realmente heterogéneos. Si necesitas una única columna de valor, la regla de fusión la decides tú, y esa regla es conocimiento de dominio que ningún aplanador genérico puede aportar.

Las Referencias Sobreviven al Aplanado Como Cadenas

Una fila plana no puede contener una relación, solo un puntero a ella. Observation.subject.reference se aplana a una cadena como Patient/123 o urn:uuid:8f2c…, que es exactamente el aspecto de una clave foránea en una tabla relacional. Eso hace cruzables las tablas por tipo: exporta Patients con clave id, exporta Observations con su subject.reference y el cruce es inmediato.

Lo que el aplanado no puede decirte es si ese puntero resuelve. La resolución es un asunto aparte, y estrictamente local cuando hay datos de paciente de por medio: el Explorador de Bundles compara las referencias únicamente con recursos del mismo fichero —por Tipo/id, por id desnudo, por fullUrl y por el Tipo/id final de un fullUrl— y nunca descarga nada por red. Las referencias que no encajan con nada se muestran como no resueltas en lugar de desaparecer sin ruido. Para el panorama completo de referencias literales frente a lógicas, consulta Cómo funcionan las referencias FHIR; para lo que se rompe específicamente dentro de un bundle de transacción, Depurar un bundle de transacción FHIR.

Escala, y Dónde el Aplanado Deja de Ser la Herramienta Adecuada

Aplanar en el navegador tiene un techo, y una herramienta honesta lo declara. El Explorador de Bundles admite hasta 25 MB de entrada y 20.000 recursos, y cuando se alcanza un límite informa de cuántos recursos se descartaron en lugar de presentar una tabla corta en silencio. Por encima de eso, el trabajo pertenece a una tubería: leer el NDJSON en streaming, aplanar línea a línea y escribir a Parquet o a una base de datos.

También conviene ser claro sobre lo que el aplanado no es. Un aplanador lee; no valida. No te dirá que falta un elemento obligatorio, que un código no pertenece a su value set o que un recurso incumple un perfil: esas son preguntas de conformidad y corresponden a un validador. El aplanado responde a otra pregunta: qué hay realmente en este fichero y puedo verlo en filas.

Una Secuencia de Trabajo

En conjunto el proceso es corto. Parsea la entrada: un Bundle, un recurso suelto o NDJSON de una exportación masiva. Agrupa por resourceType, porque una tabla que mezcla Patients y Observations no tiene un conjunto de columnas coherente. Enumera las rutas hoja con los índices colapsados y meta, text y extension excluidos. Elige columnas: un preset curado si encaja, si no las rutas más pobladas, con "mostrar todas" a un clic. Escoge join o expand según si el destino cuenta recursos o valores. Exporta usando las rutas completas como cabeceras.

Cada uno de esos pasos es una decisión sobre fidelidad y ninguno tiene una respuesta universalmente correcta, y por eso un aplanador que las oculta es más peligroso que uno que las expone. Carga un fichero en el Explorador de Bundles FHIR, alterna entre join y expand sobre los mismos datos y el compromiso deja de ser abstracto. Como todo se ejecuta en tu navegador, puedes hacerlo con registros reales sin que ninguno salga de tu equipo.

← Volver al Blog