fhirviews tiene dos partes que se usan juntas:
- un cliente FHIR (
fhir_*) para hablar con un servidor HL7 FHIR R4 desde R, como harías con Postman; - un motor de vistas (
vista_*) que convierte recursos FHIR en tablas con el estándar SQL on FHIR (ViewDefinition).
Esta viñeta recorre las dos con el caso clínico ficticio del taller aprende-fhir: Luis Alberto Paredes Cevallos, nacido en Riobamba y residente en Quito, que consulta por una intoxicación alimentaria y luego es diagnosticado y tratado por un cáncer de mama.
1. Conectarse a un servidor
Los ejemplos que necesitan un servidor no se ejecutan al construir
esta viñeta; puedes probarlos con el servidor público de HAPI o con uno
local (por ejemplo el del taller, que se levanta con Podman o Docker en
http://localhost:8080/fhir).
con <- fhir_conectar("https://hapi.fhir.org/baseR4")
con
#> <fhir_conexion> https://hapi.fhir.org/baseR4
#> Servidor: HAPI FHIR Server 8.x
#> Versión FHIR: 4.0.1
# Buscar: sigue sola los enlaces "next" de cada página
pacientes <- fhir_buscar(con, "Patient", family = "Paredes", `_count` = 50)
# Leer, crear, actualizar, borrar
paciente <- fhir_leer(con, "Patient", "123")
nuevo <- fhir_crear(con, "paciente.json")
nuevo$telecom <- list(list(system = "phone", value = "0990000000"))
nuevo <- fhir_actualizar(con, nuevo)
fhir_borrar(con, nuevo)
# Operaciones
todo <- fhir_everything(con, "Patient", "123")
fhir_validar(con, "paciente.json")
# Cualquier otra petición, como en Postman
fhir_peticion(con, "GET", "Patient/123/_history")
fhir_ejecutar_texto("GET Observation?code=http://loinc.org|85354-9&_count=5")Si el servidor responde con un error, el mensaje muestra el
OperationOutcome:
fhir_leer(con, "Patient", "no-existe")
#> Error:
#> ! El servidor respondió 404 Not Found.
#> ✖ GET https://hapi.fhir.org/baseR4/Patient/no-existe
#> • [error] HAPI-2001: Resource Patient/no-existe is not knownEn RStudio, fhir_conectar() agrega además el servidor al
panel Connections, y el menú Addins tiene Ejecutar
petición FHIR y Ejecutar ViewDefinition para lanzar con un
clic el texto seleccionado.
2. Los datos del caso clínico
El paquete incluye el caso del taller (60 recursos) en formato
NDJSON, un recurso por línea, como lo exporta la operación
$export de FHIR. Con fhir_recursos() se lee
igual que un Bundle, un archivo JSON o la respuesta de
fhir_buscar():
caso <- fhir_recursos(system.file("extdata", "caso_clinico.ndjson",
package = "fhirviews"))
caso
#> <fhir_recursos> 60 recursos
#> AllergyIntolerance: 1
#> Appointment: 2
#> AppointmentResponse: 1
#> BodyStructure: 1
#> CarePlan: 1
#> CareTeam: 1
#> Condition: 2
#> Consent: 1
#> DiagnosticReport: 2
#> Encounter: 3
#> FamilyMemberHistory: 2
#> Goal: 2
#> Immunization: 3
#> Location: 2
#> MedicationAdministration: 2
#> MedicationRequest: 2
#> MedicationStatement: 1
#> Observation: 12
#> Organization: 2
#> Patient: 1
#> Practitioner: 3
#> PractitionerRole: 2
#> Procedure: 1
#> Questionnaire: 1
#> QuestionnaireResponse: 2
#> Schedule: 1
#> ServiceRequest: 3
#> Slot: 2
#> Specimen: 1fhir_tabla() da un primer vistazo rápido, una columna
por elemento:
fhir_tabla(caso, tipo = "Patient")[, c("id", "active", "gender", "birthDate")]
#> id active gender birthDate
#> 1 patient TRUE male 1979-03-123. Probar expresiones FHIRPath
Las columnas de una vista son expresiones FHIRPath. Con
vista_fhirpath() se prueban antes de usarlas:
paciente <- caso[1]
vista_fhirpath(paciente, "name.where(use = 'official').text")
#> [1] "Luis Alberto Paredes Cevallos"
vista_fhirpath(paciente, "telecom.where(system = 'email').value")
#> [1] "luis.paredes@correo.invalid"
vista_fhirpath(paciente, "identifier.where(type.text = 'Cédula de identidad').value")
#> [1] "9900000017"
vista_fhirpath(paciente, "name.family.extension('http://hl7.org/fhir/StructureDefinition/humanname-mothers-family').value")
#> [1] "Cevallos"4. Una vista sencilla
vista_crear() arma un ViewDefinition a
partir de un vector con nombre: cada nombre es una columna y cada valor,
su expresión. Por defecto agrega la clave del recurso en la columna
id.
v_paciente <- vista_crear("Patient", c(
nombre = "name.where(use = 'official').text",
sexo = "gender",
nacimiento = "birthDate",
ciudad = "address.where(use = 'home').city"
))
v_paciente
#> <ViewDefinition>
#> Recurso: Patient
#> Columnas (5): id, nombre, sexo, nacimiento, ciudad
vista_ejecutar(caso, v_paciente)
#> id nombre sexo nacimiento ciudad
#> 1 patient Luis Alberto Paredes Cevallos male 1979-03-12 QuitoCon por_cada (el forEach de SQL on FHIR) se
obtiene una fila por cada elemento de una lista, por ejemplo los medios
de contacto:
vista_ejecutar(caso, vista_crear(
"Patient", c(medio = "system", valor = "value", uso = "use"),
por_cada = "telecom"
))
#> id medio valor uso
#> 1 patient phone +593 99 000 0000 mobile
#> 2 patient email luis.paredes@correo.invalid home5. Signos vitales con un ViewDefinition en JSON
Para vistas más ricas se escribe el ViewDefinition en
JSON, igual que en cualquier otra implementación de SQL on FHIR. Esta
vista toma las observaciones con valor numérico y, gracias a
unionAll, también los componentes de la presión arterial
(sistólica y diastólica):
signos <- vista_leer(r"-({
"resourceType": "ViewDefinition",
"name": "signos_vitales",
"resource": "Observation",
"select": [
{"column": [
{"name": "id", "path": "getResourceKey()"},
{"name": "paciente", "path": "subject.getReferenceKey(Patient)"},
{"name": "encuentro", "path": "encounter.getReferenceKey(Encounter)"},
{"name": "fecha", "path": "effective.ofType(dateTime)", "type": "dateTime"}
]},
{"unionAll": [
{"column": [
{"name": "medida", "path": "code.text"},
{"name": "loinc", "path": "code.coding.where(system = 'http://loinc.org').code.first()"},
{"name": "valor", "path": "value.ofType(Quantity).value", "type": "decimal"},
{"name": "unidad", "path": "value.ofType(Quantity).unit"}
]},
{"forEach": "component", "column": [
{"name": "medida", "path": "code.text"},
{"name": "loinc", "path": "code.coding.where(system = 'http://loinc.org').code.first()"},
{"name": "valor", "path": "value.ofType(Quantity).value", "type": "decimal"},
{"name": "unidad", "path": "value.ofType(Quantity).unit"}
]}
]}
],
"where": [{"path": "category.coding.where(code = 'vital-signs').exists()"}]
})-")
sv <- vista_ejecutar(caso, signos)
sv[!is.na(sv$valor), c("medida", "loinc", "valor", "unidad", "fecha")]
#> medida loinc valor unidad
#> 2 Presión sistólica 8480-6 128.00 mmHg
#> 3 Presión diastólica 8462-4 84.00 mmHg
#> 4 Temperatura corporal 8310-5 38.40 °C
#> 5 Frecuencia respiratoria 9279-1 20.00 respiraciones/min
#> 6 Frecuencia cardíaca 8867-4 102.00 latidos/min
#> 7 Superficie corporal 8277-6 1.77 m2
#> fecha
#> 2 2025-03-20T07:30:00-05:00
#> 3 2025-03-20T07:30:00-05:00
#> 4 2025-03-20T07:30:00-05:00
#> 5 2025-03-20T07:30:00-05:00
#> 6 2025-03-20T07:30:00-05:00
#> 7 2025-07-016. Tratamientos y vacunas
vista_ejecutar(caso, vista_crear("MedicationRequest", c(
medicamento = "medication.ofType(CodeableConcept).text",
estado = "status",
fecha = "authoredOn",
motivo = "reasonReference.display.first()"
)))
#> id medicamento estado fecha
#> 1 med-req-doxo Doxorrubicina active 2025-07-01
#> 2 med-req-tamox Tamoxifeno stopped 2025-10-15
#> motivo
#> 1 Carcinoma ductal infiltrante de mama izquierda
#> 2 Carcinoma ductal infiltrante de mama izquierda
vista_ejecutar(caso, vista_crear("Immunization", c(
vacuna = "vaccineCode.text",
estado = "status",
fecha = "occurrence.ofType(dateTime)",
razon_no_aplicada = "statusReason.text"
)))
#> id vacuna estado
#> 1 08a-immunization-diferida Influenza estacional not-done
#> 2 flu-immunization Influenza estacional completed
#> 3 08e-immunization-td Td (tétanos y difteria, adultos) completed
#> fecha razon_no_aplicada
#> 1 2025-03-20T08:00:00-05:00 Precaución médica: enfermedad aguda con fiebre
#> 2 2025-04-03T10:10:00-05:00 <NA>
#> 3 2025-04-03T10:10:00-05:00 <NA>7. Vistas listas para usar
No hace falta escribir cada vista desde cero: el paquete trae un
catálogo de ViewDefinition para las tablas más comunes en
vigilancia y salud pública. Todas usan getResourceKey() en
la columna id y getReferenceKey() en las
claves de otros recursos (paciente,
encuentro…), así que las tablas se unen entre sí, y dan lo
mismo con vista_ejecutar() que con el motor DuckDB.
vista_catalogo()[, c("nombre", "recurso", "titulo")]
#> nombre recurso
#> 1 diagnosticos Condition
#> 2 encuentros Encounter
#> 3 inmunizaciones Immunization
#> 4 medicamentos MedicationRequest
#> 5 notificacion_obligatoria Condition
#> 6 pacientes Patient
#> 7 signos_vitales Observation
#> titulo
#> 1 Diagnósticos
#> 2 Encuentros
#> 3 Inmunizaciones
#> 4 Prescripciones de medicamentos
#> 5 Enfermedades de notificación obligatoria
#> 6 Pacientes
#> 7 Signos vitalesvista_plantilla() devuelve una de ellas, lista para
ejecutar:
vista_ejecutar(caso, vista_plantilla("signos_vitales"))[
, c("id", "codigo", "descripcion", "valor", "unidad")]
#> id codigo descripcion valor unidad
#> 1 obs-pa 8480-6 Presión sistólica 128.00 mmHg
#> 2 obs-pa 8462-4 Presión diastólica 84.00 mmHg
#> 3 obs-temp 8310-5 Temperatura corporal 38.40 °C
#> 4 obs-fr 9279-1 Frecuencia respiratoria 20.00 respiraciones/min
#> 5 obs-fc 8867-4 Frecuencia cardíaca 102.00 latidos/min
#> 6 obs-bsa 8277-6 Superficie corporal 1.77 m2La presión arterial se registra como un panel con dos componentes; la
vista signos_vitales los separa con unionAll
en una fila para la sistólica (8480-6) y otra para la diastólica
(8462-4).
La vista notificacion_obligatoria filtra los
diagnósticos con una lista de categorías CIE-10 guardada en la constante
codigos (por defecto dengue, malaria y tuberculosis). Con
los 755 casos de dengue simulados de 2025 y la vista
pacientes, que trae el cantón y la provincia de residencia,
se arma la tabla de casos por provincia:
dengue <- system.file("extdata", "dengue_2025.ndjson.gz",
package = "fhirviews")
casos <- vista_ejecutar(dengue, vista_plantilla("notificacion_obligatoria"))
pacientes <- vista_ejecutar(dengue, vista_plantilla("pacientes"))
casos <- merge(casos, pacientes, by.x = "paciente", by.y = "id")
sort(table(casos$provincia), decreasing = TRUE)
#>
#> Guayas Manabí Los Ríos El Oro
#> 371 159 130 95Para otras enfermedades se cambia la constante, y
vista_json() muestra el JSON de cualquier vista para
copiarlo y adaptarlo:
solo_a91 <- vista_plantilla("notificacion_obligatoria",
constantes = list(codigos = "A91"))
nrow(vista_ejecutar(dengue, solo_a91))
#> [1] 548. Del servidor a la tabla en un paso
Con un servidor, fhir_vista() busca los recursos del
tipo de la vista y la aplica; los parámetros de búsqueda filtran en el
servidor:
fhir_vista(con, signos, subject = "Patient/123", category = "vital-signs")Y vista_duckdb() guarda varias vistas en DuckDB para
unirlas con SQL; se explica en la viñeta ViewDefinition en
detalle.
9. Exportación masiva para análisis poblacional
Para una población entera (vigilancia epidemiológica, indicadores)
buscar página a página es lento. Los servidores que implementan FHIR Bulk Data ofrecen la
operación $export: preparan en segundo plano un archivo
NDJSON por tipo de recurso y fhir_exportar() espera
(respetando Retry-After y mostrando el avance) y los
descarga a una carpeta. El NDJSON es justo el formato que
vista_ejecutar() procesa más rápido:
# Servidor de referencia de SMART, con datos sintéticos
bulk <- fhir_conectar("https://bulk-data.smarthealthit.org/fhir")
exp <- fhir_exportar(bulk, "paciente", tipos = c("Patient", "Condition"),
desde = as.Date("2024-01-01"))
exp
#> <fhir_exportacion> completada (nivel paciente)
#> Carpeta: /tmp/RtmpkBV5Xn/fhir_export_17a757190dee2
#> Condition 1 archivo, 639 recursos
#> Patient 1 archivo, 100 recursos
diagnosticos <- vista_ejecutar(exp$archivos$Condition, vista_crear(
"Condition",
c(paciente = "subject.getReferenceKey(Patient)",
codigo = "code.coding.first().code")
))nivel elige qué exportar: todo el servidor
("sistema"), todos los pacientes ("paciente")
o los de un Group (grupo = "id");
filtros pasa búsquedas por tipo (_typeFilter).
Si la exportación tarda, se puede lanzar sin esperar y volver más
tarde:
trabajo <- fhir_exportar(bulk, grupo = "brote-dengue", esperar = FALSE)
trabajo <- fhir_exportar_estado(trabajo) # consulta una vez
trabajo <- fhir_exportar_estado(trabajo, esperar = TRUE)
fhir_exportar_cancelar(trabajo) # o cancelarla10. Subir el caso a un servidor
Los recursos se pueden cargar en un servidor propio (no en el
público) para practicar con el taller. Como los ids son fijos,
fhir_actualizar() los crea con ese id en un servidor
HAPI:
local <- fhir_conectar("http://localhost:8080/fhir")
for (r in caso) fhir_actualizar(local, r)
fhir_everything(local, "Patient", "patient")