Are you tired of jumping between five different apps just to see how your sleep affected your glucose levels? Welcome to the world of Quantified Self. In this era of wearable overload, the real challenge isn't collecting data—it's data engineering and synthesis.
In this tutorial, we are going to build a unified health dashboard using Streamlit, ECharts, and Pandas. We'll tackle the "heterogeneous data problem" by aligning time-series data from an Oura Ring, Apple Watch, and a Continuous Glucose Monitor (CGM). By the end of this guide, you’ll have a professional-grade Quantified Self visualization tool that turns raw health metrics into actionable insights.
The Architecture: From Raw Logs to Insights
Handling health data is tricky because every device has a different sampling rate. Your CGM might ping every 5 minutes, while your Oura Ring provides a single "Sleep Score" once a day.
Here is how our data pipeline looks:
graph TD
A[Oura Ring API] -->|Daily Metrics| B[(SQL Database)]
C[Apple Watch / HealthKit] -->|Heart Rate/Steps| B
D[Continuous Glucose Monitor] -->|5-min Glucose| B
B --> E{Data Alignment Layer}
E -->|Pandas Resampling| F[Unified DataFrame]
F --> G[Streamlit Frontend]
G --> H[ECharts Interactive Viz]
Prerequisites
To follow along, make sure you have the following tech stack installed:
- Python 3.9+
- Streamlit: For the UI.
- Pandas: For the heavy-lifting data manipulation.
- SQLAlchemy: To interface with our health data warehouse.
- streamlit-echarts: For those beautiful, snappy charts.
pip install streamlit pandas sqlalchemy streamlit-echarts
Step 1: The Data Layer (SQLAlchemy)
First, we need to pull our data from a centralized store. For this example, we’ll assume you’ve synced your health data into a SQLite or PostgreSQL database.
import pandas as pd
from sqlalchemy import create_engine
# Database connection
engine = create_engine('sqlite:///health_data.db')
def load_health_data():
# Fetching metrics from different sources
cgm_df = pd.read_sql("SELECT timestamp, glucose_value FROM cgm_logs", engine)
activity_df = pd.read_sql("SELECT date, sleep_score, readiness_score FROM oura_metrics", engine)
# Ensure datetime types
cgm_df['timestamp'] = pd.to_datetime(cgm_df['timestamp'])
activity_df['date'] = pd.to_datetime(activity_df['date'])
return cgm_df, activity_df
Step 2: The Alignment Magic (Pandas)
This is where the Data Engineering happens. We need to align the 5-minute CGM data with the daily Oura scores. We'll use Pandas' resample and merge_asof to create a continuous timeline.
def align_data(cgm, activity):
# Resample CGM to hourly averages to reduce noise
cgm_hourly = cgm.set_index('timestamp').resample('H').mean().reset_index()
# Merge daily activity data back into the hourly timeline
# We use a left join to keep the high-frequency timeline intact
merged = pd.merge_asof(
cgm_hourly.sort_values('timestamp'),
activity.sort_values('date'),
left_on='timestamp',
right_on='date',
direction='backward'
)
return merged
Step 3: Visualizing with ECharts
Standard charts are boring. For health data, we want interactivity. ECharts allows us to zoom into specific nights where our sleep was poor and see exactly what our blood sugar was doing.
from streamlit_echarts import st_echarts
def render_health_chart(df):
options = {
"title": {"text": "Glucose vs. Sleep Correlation"},
"tooltip": {"trigger": "axis"},
"legend": {"data": ["Glucose", "Sleep Score"]},
"xAxis": {"type": "category", "data": df['timestamp'].dt.strftime('%m-%d %H:%M').tolist()},
"yAxis": [
{"type": "value", "name": "Glucose (mg/dL)"},
{"type": "value", "name": "Score", "max": 100}
],
"series": [
{
"name": "Glucose",
"type": "line",
"data": df['glucose_value'].tolist(),
"smooth": True,
"itemStyle": {"color": "#ff4d4f"}
},
{
"name": "Sleep Score",
"type": "bar",
"yAxisIndex": 1,
"data": df['sleep_score'].tolist(),
"itemStyle": {"color": "#1890ff", "opacity": 0.3}
}
],
"dataZoom": [{"type": "slider"}] # Let's add that zoom!
}
st_echarts(options=options, height="500px")
The "Official" Way to Scale
Building a local dashboard is a great start, but when you're moving from a weekend project to a production-ready health-tech platform, you'll need to handle real-time synchronization, HIPAA compliance, and more robust data pipelines.
For advanced architectural patterns and more production-ready examples of how to handle large-scale time-series data, I highly recommend checking out the technical deep dives at the WellAlly Blog. They cover everything from biometric data security to high-performance data modeling that goes far beyond a simple Streamlit app.
Putting it All Together
Finally, we wrap everything in a Streamlit app structure:
import streamlit as st
def main():
st.set_page_config(page_title="Quantified Self Dashboard", layout="wide")
st.title("📊 My Health Data Universe")
st.sidebar.header("Filters")
date_range = st.sidebar.date_input("Select Date Range")
# Load and process
cgm, activity = load_health_data()
aligned_df = align_data(cgm, activity)
# UI Layout
col1, col2 = st.columns(2)
with col1:
st.metric("Avg Glucose", f"{aligned_df['glucose_value'].mean():.1f} mg/dL")
with col2:
st.metric("Avg Sleep Score", f"{aligned_df['sleep_score'].mean():.0f}")
render_health_chart(aligned_df)
if __name__ == "__main__":
main()
Conclusion
By unifying your data, you stop looking at isolated numbers and start seeing patterns. Maybe your blood sugar spikes on nights you get less than 2 hours of REM sleep? Now you have the data to prove it!
What's next?
- Add Anomalous Detection using Scikit-learn to flag high-glucose events.
- Integrate Apple Health XML exports for even more granularity.
- Check out the WellAlly Blog for more inspiration on health-tech engineering.
Happy hacking!
Did you find this helpful? Drop a comment below with which health metric you track the most! 👇
Top comments (0)