Sunday, January 10, 2021

Google Cloud Healthcare - Analytics

Summary

This post is a continuation of my previous on the Google Healthcare API.  In this post, I'll push the FHIR datastore into Google's data warehouse - BigQuery.  Once in BigQuery, the data can then be subjected to traditional analytics tools (SQL queries) and visualized with Google's report/dashboard tool - Data Studio.  For the purposes of these demos, I extended the Synthea-generated recordsets to 50 patient bundles.

Architecture

Below is a diagram of the cloud architecture.  FHIR data (JSON-based) is transformed into relational database tables on BigQuery.  SQL queries can then be created to analyze the data.  Finally, the output of those queries can be saved as Views and then presented in charts in Data Studio.


BigQuery Execution

FHIR Export

Below is the gcloud command-line to export an FHIR datastore to BigQuery.  This is a one-time export; however, it is possible to configure a continuous stream of updates from the FHIR store to BigQuery as well.

gcloud healthcare fhir-stores export bq $FHIR_STORE_ID \
  --dataset=$DATASET_ID \
  --location=$LOCATION \
  --bq-dataset=bq://$PROJECT_ID.$BIGQUERY_DATASET_ID \
  --schema-type=analytics

Query 1 - Top Ten Medications

At this point, a relational database is created within BigQuery and ready for analytics.  Below are a query and its output to find the top 10 prescribed meds within the FHIR datastore.


Query 2 - Demographics

Below is a query that provides a bucketing of the patient age groups.


Query 3 - Top Ten Conditions

Below is a query to derive the top 10 conditions within the patient population.


Views

I then created views for each of these queries.  Those views will be used for the presentation layer of the output in Data Studio.  Below is the view of the demographics query.


Data Studio Configuration

Now that the views are set up in BigQuery, it's now possible to create visualizations of them using Data Studio.  Below are the steps to do that.

Create a blank report


Select BigQuery as the data source



Select the BigQuery View


Configure the presentation


Choose the chart type


Output

Top Ten Medications


Demographics - Age Distribution


Top Ten Conditions


Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Friday, January 1, 2021

Google Cloud Healthcare - Clinic Simulation

Summary

In this post, I'll demonstrate the usage of the Google Cloud Healthcare API via a contrived scenario.  The simulation here is a clinic in Colorado that is taking on new patients.  Each patient brings with them a health history that is contained in electronic health records in an FHIR format.  These FHIR records are pushed into the Healthcare API and then available for search, analytics, etc.  I'll be using Synthea to generate artificial records. 

Scenario

Below is a depiction of the simulation at a logical level.



Architecture

Below is the overall Google Cloud architecture.  FHIR records are generated from Synthea and then uploaded to Cloud Storage.  Google Cloud Scheduler is used to simulate a stream of patients arriving at the clinic.  Cloud Scheduler triggers a Cloud Function via Pub/Sub.  That function pulls down a random number of FHIR patient transaction bundles and then executes them to load them into Cloud Healthcare.


Patient Record Generation

As mentioned previously, I'm using Synthea to generate FHIR records.  There are three (3) releases of the FHIR standard 'supported' by the Healthcare API.  Based on my experience here, Google is not supporting the latest release (R4) adequately.  There were enough issues with Synthea-generated R4 bundles with Google that I gave up on troubleshooting them and reverted to the previous FHIR rev - STU3.  Those work.

Below is the config I used for this simulation.  This yields STU3 FHIR resource bundles with all living patients.  The default Synthea config will generate a random number of deceased patients.  In the spirit of this simulation, it's not likely any deceased patients will be admitting themselves to the clinic.
generate.only_alive_patients = true

exporter.fhir.transaction_bundle = true
exporter.fhir.export = false
exporter.fhir_stu3.export = true
exporter.fhir_dstu2.export = false

exporter.hospital.fhir.export = false
exporter.hospital.fhir_stu3.export = true
exporter.hospital.fhir_dstu2.export = false

exporter.practitioner.fhir.export = false
exporter.practitioner.fhir_stu3.export = true
exporter.practitioner.fhir_dstu2.export = false
Below is the command line I used (Developer) and a snippet of the output.  I generated ten living patients in Colorado.

./run_synthea -p 10 Colorado -c ./config.txt

Population: 10
Seed: 1609530634098
Provider Seed:1609530634098
Reference Time: 1609530634098
Location: Colorado
Min Age: 0
Max Age: 140
5 -- Bradford382 Marks830 (1 y/o M) Security-Widefield, Colorado 
4 -- Laveta191 Becker968 (2 y/o F) Denver, Colorado 
2 -- Valene773 Shanahan202 (17 y/o F) Denver, Colorado 
6 -- Valrie435 Cruickshank494 (47 y/o F) Highlands Ranch, Colorado 
1 -- Chance908 Murray856 (56 y/o M) Pueblo, Colorado 
3 -- Lashawnda573 Hettinger594 (70 y/o F) Thornton, Colorado 
8 -- Shin962 Bahringer146 (69 y/o F) Denver, Colorado 
7 -- Johnie961 Padberg411 (68 y/o M) Thornton, Colorado 
10 -- Bridget51 Leffler128 (24 y/o F) Louisville, Colorado 
9 -- Carlos172 González124 (55 y/o M) Denver, Colorado 
Records: total=10, alive=10, dead=0

Google Cloud Configuration

Screenshots below of the various upfront configuration activities that were necessary for this simulation:

Cloud Storage



Cloud Healthcare





Cloud Pub/Sub



Cloud Scheduler


Cloud Function


Hospital and Practitioner Upload

Synthea generates three types of FHIR transaction bundles for a given synthetic load:  Patient, Hospital, and Practitioner.  Patient resources reference Hospital and Practitioner resources.  Practitioner resources reference Hospital resources.  Below are the CURL commands I used to upload the Hospital and Practitioner bundles.  Note that I'm using 'v1beta1' of the Healthcare API.  V1 doesn't handle conditional references properly.  The Synthea bundles have conditional references in them.

curl -X POST \
    -H "Content-Type: application/fhir+json; charset=utf-8" \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    --data @hospitalInformation1609530634625.json \
    "https://healthcare.googleapis.com/v1beta1/projects/clinic-simulation/locations/us-central1/datasets/clinicset/fhirStores/clinicSTU3/fhir"


curl -X POST \
    -H "Content-Type: application/fhir+json; charset=utf-8" \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    --data @practitionerInformation1609530634625.json \
    "https://healthcare.googleapis.com/v1beta1/projects/clinic-simulation/locations/us-central1/datasets/clinicset/fhirStores/clinicSTU3/fhir"

Code Snippets

Cloud Function for Executing Transaction Bundles


/**
 * Reads a FHIR bundle from GCS and then loads it to a Google Healthcare FHIR store
 * @param {File} file - GCS File object
 * @return {Promise} 
 * @throws {Error} propagates exceptions
 */
async function loadBundle(file) {   
    const auth = await google.auth.getClient({
        scopes: ['https://www.googleapis.com/auth/cloud-platform'],
    });
    google.options({
        auth, 
        headers: {
            'Content-Type': 'application/fhir+json'
        }
    });

    let bundle = '';
    return new Promise((resolve, reject) => {
        file.createReadStream()
        .on('data', (chunk) => {
            bundle += chunk;
        })
        .on('end', async () => {
            const request = { 
                parent: BASE_URL,
                type: 'Bundle',
                requestBody: JSON.parse(bundle),
            };
            let response;

            try {
                response = await healthcare.projects.locations.datasets.fhirStores.fhir.executeBundle(request);
                resolve(response.statusText);
            }
            catch(err) {
                reject(err);
            }
        })
        .on('error', (err) => {
            reject(err);
        });
    });
}

Search Function

Code below searches the FHIR store for male patients over age 60.

async function search() { 
    const auth = await google.auth.getClient({
        scopes: ['https://www.googleapis.com/auth/cloud-platform'],
    });
    google.options({
        auth, 
        params: {
            gender:'male',
            birthdate: 'lt1961-01-01'
        }
    });
  
    const request = { 
        parent: BASE_URL, 
        resourceType : 'Patient'
    };
    const response = await healthcare.projects.locations.datasets.fhirStores.fhir.search(request);
    
    return response.data.entry;
}

Execution

Log snippet below for the Generator function executing patient transaction bundles from Cloud Storage.


The abbreviated output below from the search function mentioned previously.  Recall there was only 1 male patient over 60 in the ten patients that were loaded.

     "name": [
        {
          "family": "Padberg411",
          "given": [
            "Johnie961"
          ],
          "prefix": [
            "Mr."
          ],
          "use": "official"
        }
      ],
      "resourceType": "Patient",


Result count: 1

Source


Copyright ©1993-2024 Joey E Whelan, All rights reserved.