Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

CGM Data Analysis with JupyterHealth

Welcome to the Continuous Glucose Monitoring (CGM) tutorial on the JupyterHealth platform.
This notebook walks you through:

  • 📥 Getting patient and study information from JupyterHealth Exchange

  • 📥 Loading CGM data from JupyterHealth Exchange

  • 🧮 Calculating core AGP (Ambulatory Glucose Profile) metrics

  • 📊 Visualizing glucose trends for researchers or clinicians


🔗 Step 1: Connect to JupyterHealth Exchange and Explore

We use the jupyterhealth-client package to securely pull CGM data from a trusted JupyterHealth Data Exchange.

- 🏥 First, list available organizations

JupyterHealth Exchange organizes data access and patients are associated with organizations, which may be in a hierarchy of institutions, departments, teams, etc.

[20011] University of Example
 ⮑[20027] ACME
 ⮑[20013] Example Institute for Tutorials
 ⮑[20026] Cardiology Department
 ⮑[20024] JupyterHub users
[20001] U of X
 ⮑[20019] East Campus

-🧍🏾‍♀️📋🧍🏻‍♂️Second, list available studies and patients

A study is associated with an organization and specific data requests.

Practitioners have access to specific studies, e.g. via their organization membership.

All my studies:

  - [30012] CGM & Metabolic Health Study org:Example Institute for Tutorials
  - [30010] Health Connect CGM Integration org:Cardiology Department
  - [30013] iHealth Blood Pressure Study org:Cardiology Department
  - [30006] Remote Monitoring demo org:East Campus
  - [30011] Sample Data org:Example Institute for Tutorials

I’m interested in the ‘Sample Data’ study, with id 30011.

I’m interested in it because it’s the one with fake patients and sample data from cite (no label) so I can include the output in these docs.

{'id': 30011, 'name': 'Sample Data', 'description': 'To explore the Exchange capabilities. This study should only contain synthetic patients and already published or synthetic Observations.', 'organization': {'id': 20013, 'name': 'Example Institute for Tutorials', 'type': 'edu'}}

Reviewing patients in the study

We can review which patients are in the study, and which are invited to the study but haven’t consented to data availability via the patient consents endpoint:

Patients in study [30011] Sample Data
[40001] Patient, Demo: demouser@jupyterhealth.org
 - Blood glucose (not consented)
 - Blood pressure (not consented)
 - Heart Rate (not consented)
[40054] Nguyen, Minh: minh.nguyen@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40055] Smith, Olivia: olivia.smith@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40056] Chen, Liang: liang.chen@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40057] Patel, Anika: anika.patel@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40058] Garcia, Carlos: carlos.garcia@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40059] Okafor, Chinelo: chinelo.okafor@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40060] Kowalski, Zofia: zofia.kowalski@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40061] Tanaka, Hiroshi: hiroshi.tanaka@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40062] Abdullah, Layla: layla.abdullah@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40063] Dubois, Émile: émile.dubois@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40064] Singh, Raj: raj.singh@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40065] Martinez, Sofia: sofia.martinez@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40066] Kim, Jisoo: jisoo.kim@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40067] Ivanov, Dmitri: dmitri.ivanov@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40068] Mbatha, Sipho: sipho.mbatha@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40069] Rossi, Giulia: giulia.rossi@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40070] Hernandez, Luis: luis.hernandez@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40071] Yilmaz, Aylin: aylin.yilmaz@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17
[40072] Andersson, Lars: lars.andersson@example.com
 - Blood glucose, consented 2025-04-17
 - Blood pressure, consented 2025-04-17
 - Heart Rate, consented 2025-04-17

A patient whose data has been requested, but has not consented (there will be no data on this patient):

{'patient': {'id': 40001, 'jheUserId': 10007, 'identifier': 'demouser-min', 'nameFamily': 'Patient', 'nameGiven': 'Demo', 'birthDate': '1955-01-01', 'telecomPhone': None, 'telecomEmail': 'demouser@jupyterhealth.org', 'organizationId': 20013}, 'consolidatedConsentedScopes': [], 'studiesPendingConsent': [{'id': 30011, 'name': 'Sample Data', 'description': 'To explore the Exchange capabilities. This study should only contain synthetic patients and already published or synthetic Observations.', 'organization': {'id': 20013, 'name': 'Example Institute for Tutorials', 'type': 'edu'}, 'dataSources': [{'id': 70002, 'name': 'Dexcom', 'type': 'personal_device', 'supportedScopes': []}, {'id': 70001, 'name': 'iHealth', 'type': 'personal_device', 'supportedScopes': []}], 'pendingScopeConsents': [{'code': {'id': 50001, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-glucose:4.0', 'text': 'Blood glucose'}, 'consented': None, 'consentedTime': None}, {'code': {'id': 50002, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-pressure:4.0', 'text': 'Blood pressure'}, 'consented': None, 'consentedTime': None}, {'code': {'id': 50005, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:heart-rate:2.0', 'text': 'Heart Rate'}, 'consented': None, 'consentedTime': None}]}], 'studies': []}

A sample patient who has consented to share data with this study:

{'patient': {'id': 40054, 'jheUserId': 10040, 'identifier': '1636-69-001', 'nameFamily': 'Nguyen', 'nameGiven': 'Minh', 'birthDate': '1984-07-11', 'telecomPhone': '265-642-0143', 'telecomEmail': 'minh.nguyen@example.com', 'organizationId': 20013}, 'consolidatedConsentedScopes': [{'id': 50001, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-glucose:4.0', 'text': 'Blood glucose'}, {'id': 50002, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-pressure:4.0', 'text': 'Blood pressure'}, {'id': 50005, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:heart-rate:2.0', 'text': 'Heart Rate'}], 'studiesPendingConsent': [], 'studies': [{'id': 30011, 'name': 'Sample Data', 'description': 'To explore the Exchange capabilities. This study should only contain synthetic patients and already published or synthetic Observations.', 'organization': {'id': 20013, 'name': 'Example Institute for Tutorials', 'type': 'edu'}, 'dataSources': [{'id': 70002, 'name': 'Dexcom', 'type': 'personal_device', 'supportedScopes': []}, {'id': 70001, 'name': 'iHealth', 'type': 'personal_device', 'supportedScopes': []}], 'scopeConsents': [{'code': {'id': 50001, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-glucose:4.0', 'text': 'Blood glucose'}, 'consented': True, 'consentedTime': '2025-04-17T16:45:45.625689Z'}, {'code': {'id': 50002, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:blood-pressure:4.0', 'text': 'Blood pressure'}, 'consented': True, 'consentedTime': '2025-04-17T16:45:45.625689Z'}, {'code': {'id': 50005, 'codingSystem': 'https://w3id.org/openmhealth', 'codingCode': 'omh:heart-rate:2.0', 'text': 'Heart Rate'}, 'consented': True, 'consentedTime': '2025-04-17T16:45:45.625689Z'}]}]}

👩🏻‍🦰 Step 2: Select patient

Select the study patient you are interested in from the information above.

Now we can fetch observations into a pandas DataFrame for the data from this patient and this study.

Loading...

We can see a summary of the fields in an Observation if we look at just the first row

resource_type omh:blood-glucose:4.0 resourceType Observation id 136676 meta_lastUpdated 2025-04-17 16:45:45.625689+00:00 status final subject_reference Patient/40072 code_coding_0_code omh:blood-glucose:4.0 code_coding_0_system https://w3id.org/openmhealth modality self-reported schema_id_name blood-glucose schema_id_version 3.1 schema_id_namespace omh creation_date_time 2024-06-05 12:23:22+00:00 external_datasheets_0_datasheet_type manufacturer external_datasheets_0_datasheet_reference Dexcom source_creation_date_time 2024-06-05 12:23:22+00:00 blood_glucose_unit MGDL blood_glucose_value 129 effective_time_frame_date_time 2024-06-05 12:23:22+00:00 temporal_relationship_to_meal unknown creation_date_time_local 2024-06-05 12:23:22 source_creation_date_time_local 2024-06-05 12:23:22 effective_time_frame_date_time_local 2024-06-05 12:23:22 Name: 0, dtype: object

🧠 Step 3: Calculate Glucose Metrics

These help us evaluate patient stability and risk zones.

We now compute key statistics to understand the glucose trends:

  • Mean Glucose =∑Glucose ReadingsTotal Number of Readings = \frac{\sum \text{Glucose Readings}}{\text{Total Number of Readings}}

  • Glucose Variability (CV%) =(Standard Deviation of GlucoseMean Glucose)×100 = \left( \frac{\text{Standard Deviation of Glucose}}{\text{Mean Glucose}} \right) \times 100

  • Glucose Management Indicator (GMI) =3.31+0.02392×Mean Glucose= 3.31 + 0.02392 \times \text{Mean Glucose}

  • Time in Range (%) = (Number of readings in range of 70-180mg/dLTotal number of readings)×100\left( \frac{\text{Number of readings in range of 70-180mg/dL}}{\text{Total number of readings}} \right) \times 100

Breakdown:

  • VLow = Level 2 Hypoglycemia (<54 mg/dL)

  • Low = Level 1 Hypoglycemia (54–69 mg/dL)

  • Target Range (70–180 mg/dL)

  • High = Level 1 Hyperglycemia (181–250 mg/dL)

  • VHigh = Level 2 Hyperglycemia (>250 mg/dL)

🧠 Glycemia Risk Index (GRI) GRI is a composite metric developed to summarize the overall glycemic risk based on CGM data. It combines both hypoglycemia and hyperglycemia exposure into a single score from 0 to 100, where:

  • 0 = Ideal glycemic control (100% Time In Range)

  • 100 = Maximum glycemic risk

The GRI is computed from the percentage of time spent in four glucose ranges:

📐 GRI Calculation

GRI=3.0×VLow+2.4×Low+1.6×VHigh+0.8×High\text{GRI} = 3.0 \times \text{VLow} + 2.4 \times \text{Low} + 1.6 \times \text{VHigh} + 0.8 \times \text{High}

GRI is also visualized using a 2D GRI Grid, where:

  • X-axis = Hypoglycemia Component = VLow + (0.8 × Low)

  • Y-axis = Hyperglycemia Component = VHigh + (0.5 × High)

This allows clinicians to see whether risk is driven more by lows or highs, even if two patients have the same overall GRI.

The first block of code:

1.	✅ Confirms units are in mg/dL.
2.	🔍 Extracts just CGM glucose values and timestamps.
3.	🕐 Sorts the glucose values by time for downstream analysis.
Mean Glucose: 103.92 mg/dL
Standard Deviation (SD): 23.71 mg/dL
Coefficient of Variation (CV): 22.82%
Glucose Management Indicator (GMI): 5.80%

--- GRI COMPONENTS ---
Level 2 Hypoglycemia (<54 mg/dL): 0.15%
Level 1 Hypoglycemia (54-69 mg/dL): 4.07%
Level 1 Hyperglycemia (181-250 mg/dL): 0.70%
Level 2 Hyperglycemia (>250 mg/dL): 0.00%
Hypoglycemia Component: 3.41%
Hyperglycemia Component: 0.35%
Glycemia Risk Index (GRI): 10.78

📈 Step 4: Visualize Glucose Patterns

This section generates:

  • GRI Grid

  • Daily Glucose Profiles

  • Ambulatory Glucose Profile (AGP)

  • Goals for Type 1 and Type Diabetes

These are vital for interpreting glycemic control and variability.

🟩 Median Glucose Trend Line

The green line represents a smoothed median glucose trend for each day. It is calculated using a rolling window of 5 readings, which helps reduce short-term fluctuations and highlight overall trends in glucose levels.

  • ✅ Helps visualize daytime and nighttime patterns

  • ✅ Reduces noise from rapid sensor changes

  • ⚠️ May slightly lag or flatten sharp spikes or drops

This smoothing approach mimics how continuous glucose monitoring (CGM) systems often display data to enhance clinical interpretation.

<Figure size 800x600 with 1 Axes>
<Figure size 2500x600 with 10 Axes>

Ambulatory Glucose Profile (AGP) visualizes glucose trends as if they occurred in a single day. Colored Bands (Percentile Ranges):

  • Dark Green Line: Median glucose (50th percentile).

  • Green Shaded Area: 25th–75th percentile range.

  • Orange Shaded Area: 5th–95th percentile range.

  • Target Range (70–180 mg/dL):

  • Marked in green with horizontal lines at 70 mg/dL and 180 mg/dL.

<Figure size 1200x600 with 1 Axes>
<Figure size 800x500 with 1 Axes>
<Figure size 200x600 with 1 Axes>
<Figure size 2000x500 with 1 Axes>
Loading...

MAGE (Mean Amplitude of Glycemic Excursions)

MAGE measures large glucose fluctuations:

Mean Amplitude of Glycemic Excursions (MAGE): 37.74 mg/dL

MODD (Mean of Daily Differences)

MODD measures day-to-day glucose fluctuations at the same time of day:

Mean of Daily Differences (MODD): 14.53 mg/dL

Step 5: 📊 Aggregate study data across patients

If we omit the patient_id from the query, we will get observations for all patients in the study. The patient is identified by the subject_reference column, which looks like Patient/{patient_id}

Loading...

We can remove the Patient/ prefix in subject_reference, and subset the relevant data as we did before. This time, keeping patient_id so we can differentiate patients.

Loading...

Now we can group by patient to compare across our cohort:

<Figure size 640x480 with 1 Axes>

And again, compute our Glucose Management Indicator (GMI) from the mean:

<Figure size 640x480 with 1 Axes>

We can also use this information to produce time-in-range bar for every patient in the study

Loading...

Data Sources and Methodology:

This tutorial uses sample CGM data from the PLOS Biology supplemental dataset: cite (no label)

Analysis methods are based on:

References
  1. Hall, H., Perelman, D., Breschi, A., Limcaoco, P., Kellogg, R., McLaughlin, T., & Snyder, M. (2018). Glucotypes reveal new patterns of glucose dysregulation. PLOS Biology, 16(7), e2005143. 10.1371/journal.pbio.2005143