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)

  1. Design de formulário (30 min): Criação interface e validações
  2. Configuração avançada (30 min): Regras de cálculo e alertas
  3. Integração de dados (30 min): Upload via API
  4. 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

  1. Formulário REDCap exportado com anotações HL7 FHIR
  2. Script Python para integração automática
  3. HL7 FHIR Bundle validado com dados reais
  4. Manual de utilizador para o workflow completo
  5. 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.status não precisam ser explicitamente anotados se forem incluídos diretamente via código.
  • É recomendável usar o record_id (ou similar) para preencher automaticamente o subject.reference.
  • Campos de comentários pode serem usados como MedicationStatement.note[0].text.

Ficheiros úteis