Experiência 3: Armazenamento Estruturado de Dados
Experiência 3: Armazenamento Estruturado de Dados Biomédicos
Objetivo
Capacitar os estudantes na estruturação, validação e armazenamento de dados biomédicos utilizando plataformas de Electronic Data Capture (EDC) como o REDCap. A experiência enfatiza a importância da qualidade dos dados, metadados estruturados e interoperabilidade através de mapeamento para padrões HL7 FHIR.
Objetivos de Aprendizagem
- Projetar formulários eletrónicos para dados biomédicos estruturados
- Implementar validações e controlos de qualidade de dados
- Compreender a importância de metadados e anotações semânticas
- Mapear dados clínicos para recursos HL7 FHIR padronizados
- Configurar workflows de exportação e integração de dados
- Aplicar princípios de FAIR data (Findable, Accessible, Interoperable, Reusable)
Resultados de Aprendizagem
Ao completar esta experiência, os estudantes serão capazes de:
- Criar formulários RedCap com validações robustas
- Implementar anotações HL7 FHIR para interoperabilidade
- Configurar regras de cálculo e lógica condicional
- Exportar dados estruturados via API
- Converter dados para formato HL7 FHIR Bundle válido
- Implementar controlos de qualidade e auditoria
Exercício Prático Detalhado
Fase 1: Design do Formulário de Sinais Vitais (45 min)
Objetivo específico:
Criar formulário para armazenar dados processados das Experiências 1 e 2, incluindo sinais brutos e características extraídas.
Estrutura do formulário:
Formulário: "Monitorização Fisiológica"
Secção 1: Identificação do Paciente
- record_id (auto-incremento)
- patient_id (texto, obrigatório)
- data_collection (data/hora)
- device_id (dropdown: ESP32_001, ESP32_002, ...)
Secção 2: Parâmetros de Aquisição
- sampling_rate (numérico, min:50, max:1000)
- duration_minutes (numérico, min:1, max:60)
- sensor_types (checkbox: ECG, PPG, GSR, Temp)
- signal_quality (dropdown: Excelente, Boa, Aceitável, Pobre)
Secção 3: Sinais Vitais Calculados
- heart_rate_bpm (numérico, min:40, max:200)
- temperature_celsius (numérico, min:35, max:42)
- spo2_percentage (numérico, min:70, max:100)
- respiratory_rate (numérico, min:8, max:40)
Secção 4: Métricas HRV
- mean_rr_ms (numérico)
- sdnn_ms (numérico)
- rmssd_ms (numérico)
- pnn50_percent (numérico, min:0, max:100)
Secção 5: Dados Brutos (attachments)
- raw_ecg_file (upload CSV)
- processed_signals_file (upload JSON)
- analysis_report (upload PDF)
Anotações HL7 FHIR para cada campo:
# Campos de identificação
patient_id: @fhir:Patient.identifier[0].value
data_collection: @fhir:Observation.effectiveDateTime
# Sinais vitais mapeados para Observation
heart_rate_bpm: @fhir:Observation.valueQuantity.value|code=8867-4
temperature_celsius: @fhir:Observation.valueQuantity.value|code=8310-5
spo2_percentage: @fhir:Observation.valueQuantity.value|code=59408-5
# Dispositivo como Device resource
device_id: @fhir:Device.identifier[0].value
# Qualidade como Observation.dataAbsentReason
signal_quality: @fhir:Observation.dataAbsentReason.coding[0].display
Fase 2: Implementação de Validações Avançadas (30 min)
Regras de validação automática:
// Validação cruzada: FC vs temperatura
if([heart_rate_bpm] > 100 && [temperature_celsius] > 38.0) {
// Marcar como "possível febre" automaticamente
[clinical_alert] = "1";
}
// Cálculo automático de IMC se peso/altura disponíveis
if([weight_kg] <> "" && [height_cm] <> "") {
[bmi_calculated] = [weight_kg] / (([height_cm]/100) * ([height_cm]/100));
}
// Validação de consistência HRV
if([mean_rr_ms] <> "" && [heart_rate_bpm] <> "") {
$expected_hr = 60000 / [mean_rr_ms];
if(abs($expected_hr - [heart_rate_bpm]) > 10) {
[validation_warning] = "Inconsistência entre RR intervals e BPM";
}
}Configuração de alertas:
- Valores críticos: FC < 50 ou > 150 BPM
- Dados em falta: Sinais obrigatórios não preenchidos
- Inconsistências: Valores fisiologicamente implausíveis
- Qualidade: Score de qualidade < 70%
Fase 3: Integração com Dados das Experiências Anteriores (45 min)
Script Python para upload automático:
import requests
import json
import pandas as pd
from datetime import datetime
class RedCapIntegration:
def __init__(self, api_url, api_token):
self.api_url = api_url
self.api_token = api_token
def upload_sensor_data(self, sensor_data, processed_metrics):
"""
Upload dados das Experiências 1 e 2 para RedCap
"""
# Preparar dados no formato RedCap
record_data = {
'record_id': self.get_next_record_id(),
'patient_id': f"PATIENT_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
'data_collection': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'device_id': sensor_data.get('device_id', 'ESP32_001'),
'sampling_rate': sensor_data.get('sampling_rate', 100),
'duration_minutes': len(sensor_data['ecg']) / (sensor_data['sampling_rate'] * 60),
# Métricas calculadas na Experiência 2
'heart_rate_bpm': processed_metrics['heart_rate'],
'mean_rr_ms': processed_metrics['hrv_metrics']['mean_rr'],
'sdnn_ms': processed_metrics['hrv_metrics']['sdnn'],
'rmssd_ms': processed_metrics['hrv_metrics']['rmssd'],
'pnn50_percent': processed_metrics['hrv_metrics']['pnn50'],
# Qualidade do sinal
'signal_quality': self.assess_signal_quality(processed_metrics['quality_score'])
}
# Upload via API RedCap
response = self.upload_record(record_data)
# Upload ficheiros anexos
if 'raw_data_file' in sensor_data:
self.upload_file(record_data['record_id'], 'raw_ecg_file', sensor_data['raw_data_file'])
return response
def upload_record(self, data):
"""Upload de um registo via API RedCap"""
payload = {
'token': self.api_token,
'content': 'record',
'action': 'import',
'format': 'json',
'type': 'flat',
'data': json.dumps([data])
}
response = requests.post(self.api_url, data=payload)
return response.json()
def assess_signal_quality(self, quality_score):
"""Converter score numérico para categoria"""
if quality_score >= 90:
return "1" # Excelente
elif quality_score >= 75:
return "2" # Boa
elif quality_score >= 60:
return "3" # Aceitável
else:
return "4" # Pobre
# Exemplo de utilização
redcap = RedCapIntegration(
api_url="https://redcap.med.up.pt/api/",
api_token="YOUR_API_TOKEN"
)
# Carregar dados das experiências anteriores
sensor_data = load_arduino_data("esp32_session_001.json")
processed_metrics = load_processing_results("processed_metrics.json")
# Upload para RedCap
result = redcap.upload_sensor_data(sensor_data, processed_metrics)
print(f"Upload completo: {result}")Fase 4: Exportação HL7 FHIR Avançada (60 min)
Converter dados RedCap para Bundle HL7 FHIR completo:
def create_fhir_bundle_from_redcap(redcap_data):
"""
Converte dados RedCap para HL7 FHIR Bundle com múltiplos recursos
"""
bundle = {
"resourceType": "Bundle",
"id": f"bundle-{datetime.now().strftime('%Y%m%d%H%M%S')}",
"type": "collection",
"timestamp": datetime.now().isoformat() + "Z",
"entry": []
}
for record in redcap_data:
# 1. Criar Patient resource
patient = create_patient_resource(record)
bundle["entry"].append({"resource": patient})
# 2. Criar Device resource
device = create_device_resource(record)
bundle["entry"].append({"resource": device})
# 3. Criar múltiplas Observations
observations = create_observations_from_vitals(record, patient["id"], device["id"])
for obs in observations:
bundle["entry"].append({"resource": obs})
# 4. Criar DiagnosticReport agregando todas as observações
diagnostic_report = create_diagnostic_report(record, patient["id"], observations)
bundle["entry"].append({"resource": diagnostic_report})
return bundle
def create_observations_from_vitals(record, patient_id, device_id):
"""Criar observações individuais para cada sinal vital"""
observations = []
# Heart Rate Observation
if record.get('heart_rate_bpm'):
hr_obs = {
"resourceType": "Observation",
"id": f"hr-{record['record_id']}",
"status": "final",
"category": [{
"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs"}]
}],
"code": {
"coding": [{"system": "http://loinc.org", "code": "8867-4", "display": "Heart rate"}]
},
"subject": {"reference": f"Patient/{patient_id}"},
"effectiveDateTime": record['data_collection'],
"valueQuantity": {
"value": float(record['heart_rate_bpm']),
"unit": "beats/min",
"system": "http://unitsofmeasure.org",
"code": "/min"
},
"device": {"reference": f"Device/{device_id}"}
}
observations.append(hr_obs)
# Temperature Observation
if record.get('temperature_celsius'):
temp_obs = {
"resourceType": "Observation",
"id": f"temp-{record['record_id']}",
"status": "final",
"category": [{
"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "vital-signs"}]
}],
"code": {
"coding": [{"system": "http://loinc.org", "code": "8310-5", "display": "Body temperature"}]
},
"subject": {"reference": f"Patient/{patient_id}"},
"effectiveDateTime": record['data_collection'],
"valueQuantity": {
"value": float(record['temperature_celsius']),
"unit": "Cel",
"system": "http://unitsofmeasure.org",
"code": "Cel"
},
"device": {"reference": f"Device/{device_id}"}
}
observations.append(temp_obs)
# HRV Metrics como Observation complexa
if any(record.get(k) for k in ['mean_rr_ms', 'sdnn_ms', 'rmssd_ms']):
hrv_obs = create_hrv_observation(record, patient_id, device_id)
observations.append(hrv_obs)
return observations
def create_hrv_observation(record, patient_id, device_id):
"""Criar observação composta para métricas HRV"""
hrv_components = []
if record.get('mean_rr_ms'):
hrv_components.append({
"code": {"coding": [{"system": "http://snomed.info/sct", "code": "364075005", "display": "Mean RR interval"}]},
"valueQuantity": {"value": float(record['mean_rr_ms']), "unit": "ms"}
})
if record.get('sdnn_ms'):
hrv_components.append({
"code": {"coding": [{"system": "http://snomed.info/sct", "code": "SDNN", "display": "Standard deviation of NN intervals"}]},
"valueQuantity": {"value": float(record['sdnn_ms']), "unit": "ms"}
})
return {
"resourceType": "Observation",
"id": f"hrv-{record['record_id']}",
"status": "final",
"category": [{
"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category",
"code": "survey"}]
}],
"code": {
"coding": [{"system": "http://snomed.info/sct", "code": "251874006", "display": "Heart rate variability"}]
},
"subject": {"reference": f"Patient/{patient_id}"},
"effectiveDateTime": record['data_collection'],
"component": hrv_components,
"device": {"reference": f"Device/{device_id}"}
}Recursos Necessários
Plataforma e Software
- REDCap (instância local ou na nuvem)
- Python 3.8+ com bibliotecas:
- requests (API calls)
- pandas (manipulação dados)
- fhir.resources (validação HL7 FHIR)
- pycap (REDCap Python client)
- Google Colab ou Jupyter Notebook
- Postman para testes de API
Dados de Input
- Dados brutos das Experiências 1 e 2
- Métricas processadas (HRV, qualidade de sinal)
- Metadados de dispositivos e sessões
- Templates de formulários REDCap pré-configurados
Infraestrutura
- Servidor REDCap configurado
- Base de dados MySQL/MariaDB
- Backup e disaster recovery
- Certificados SSL para API segura
Metodologias de Ensino
Sessão Teórica (30 min)
- Princípios de EDC e qualidade de dados
- Padrões de metadados em investigação biomédica
- HL7 FHIR fundamentals e recursos relevantes
- Aspectos legais (GDPR, consentimento informado)
Sessão Prática (120 min)
- Design de formulário (30 min): Criação interface e validações
- Configuração avançada (30 min): Regras de cálculo e alertas
- Integração de dados (30 min): Upload via API
- Exportação HL7 FHIR (30 min): Conversão e validação
Trabalho de Grupo (60 min)
- Implementar validações customizadas
- Criar dashboards de qualidade de dados
- Configurar notificações automáticas
- Documentar procedimentos de backup
Avaliação
Critérios de Avaliação (100 pontos)
- Design do formulário (25 pts): Adequação, validações, usabilidade
- Qualidade dos dados (20 pts): Completude, consistência, precisão
- Mapeamento HL7 FHIR (25 pts): Correção sintática e semântica
- Integração técnica (20 pts): API, scripts, automatização
- Documentação (10 pts): Procedimentos, troubleshooting, metadados
Entregáveis
- Formulário REDCap exportado com anotações HL7 FHIR
- Script Python para integração automática
- HL7 FHIR Bundle validado com dados reais
- Manual de utilizador para o workflow completo
- Relatório de qualidade dos dados recolhidos
Extensões Avançadas
Integração com Sistemas Externos
- API REST para recepção de dados em tempo real
- Webhooks para notificações automáticas
- ETL pipelines para data warehousing
- Machine learning para detecção de anomalias
Segurança e Compliance
- Encriptação end-to-end dos dados
- Audit logs completos
- Role-based access control (RBAC)
- Anonymização automática para investigação
Analytics Avançados
- Power BI dashboards conectados
- R/Shiny apps para análise interativa
- Apache Spark para big data processing
- Time series databases (InfluxDB) para dados temporais
Integração com Pipeline
Input das Experiências Anteriores
- Dados brutos de sensores (Experiência 1)
- Sinais processados e características (Experiência 2)
- Metadados de qualidade e calibração
- Timestamps para correlação temporal
Output para Experiências Seguintes
- Dados estruturados em formato HL7 FHIR
- APIs REST para acesso programático
- Datasets limpos para análise
- Metadados ricos para interpretação
Preparação para Interoperabilidade
- Resources HL7 FHIR válidos e testados
- Mapeamentos para terminologias padrão (LOINC, SNOMED)
- Profiles customizados para casos de uso específicos
- Implementation Guides para reutilização
Os recursos gerados podem ser agrupados em um único HL7 FHIR Bundle:
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{ "resource": { ... } },
{ "resource": { ... } }
]
}Exemplo prático
Um formulário de prescrição terapêutica gerado no REDCap pode produzir, via script, um MedicationStatement como este:
{
"resourceType": "MedicationStatement",
"status": "active",
"subject": { "reference": "Patient/52" },
"dateAsserted": "2025-06-04T11:17:00",
"medicationCodeableConcept": { "text": "Eutirox 25" },
"dosage": [
{
"method": { "text": "Comprimido ou cápsula" },
"text": "1",
"timing": {
"code": { "text": "08:00" }
}
}
]
}Notas adicionais
- Campos como
@fhir:MedicationStatement.statusnão precisam ser explicitamente anotados se forem incluídos diretamente via código. - É recomendável usar o
record_id(ou similar) para preencher automaticamente osubject.reference. - Campos de comentários pode serem usados como
MedicationStatement.note[0].text.
Ficheiros úteis
- Formulário de Plano Terapêutico com registo de medicações no REDCap’
- Script Python para Google Colab