Quantified Self: DuckDB와 Streamlit으로 초고속 건강 대시보드 만들기

발행: (2026년 2월 9일 오전 10:15 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

왜 이 스택인가?

If you’ve been following modern data stacks, you know that OLAP for wearables is becoming a hot topic. Traditional Python libraries like Pandas are great, but they often struggle with the memory overhead of large nested XML structures.

ComponentWhy It Matters
DuckDB“분석용 SQLite.” 인‑프로세스 컬럼형 데이터베이스로, 빛의 속도로 SQL을 실행합니다.
PyArrow포맷 간 데이터 이동을 위한 제로‑복사 브리지.
Streamlit데이터 스크립트를 공유 가능한 웹 앱으로 전환하는 가장 빠른 방법.

아키텍처 🏗️

웨어러블 데이터에서 가장 큰 도전은 ETL (Extract‑Transform‑Load) 프로세스입니다. 계층형 XML 파일을 DuckDB가 처리할 수 있는 평탄화된, 쿼리 가능한 Parquet 형식으로 변환해야 합니다.

graph TD
    A[Apple Health export.xml] --> B[Python XML Parser]
    B --> C[PyArrow Table]
    C --> D[Parquet Storage]
    D --> E[DuckDB Engine]
    E --> F[Streamlit Dashboard]
    F --> G[Millisecond Insights 🚀]

사전 요구 사항

시작하기 전에 export.xml 파일을 준비하고 필요한 도구가 설치되어 있는지 확인하세요:

pip install duckdb streamlit pandas pyarrow

Step 1 – XML 혼돈에서 Parquet 질서로

Apple의 XML 형식은… “특이합니다.” 우리는 PyArrow를 사용해 스키마를 정의하고 해당 레코드를 압축된 Parquet 파일로 변환합니다. 이렇게 하면 파일 크기가 최대 90 %까지 감소하고 컬럼형 읽기에 최적화됩니다.

import xml.etree.ElementTree as ET
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq

def parse_health_data(xml_path: str, parquet_path: str = "health_data.parquet"):
    """Parse Apple Health export XML and write a Parquet file."""
    tree = ET.parse(xml_path)
    root = tree.getroot()

    # Keep only Record elements for this dashboard
    records = [
        {
            "type": rec.get("type"),
            "value": rec.get("value"),
            "unit": rec.get("unit"),
            "creationDate": rec.get("creationDate"),
            "startDate": rec.get("startDate"),
        }
        for rec in root.findall("Record")
    ]

    df = pd.DataFrame(records)

    # Convert dates and numeric values
    df["startDate"] = pd.to_datetime(df["startDate"])
    df["value"] = pd.to_numeric(df["value"], errors="coerce")

    # Arrow Table → Parquet
    table = pa.Table.from_pandas(df)
    pq.write_table(table, parquet_path, compression="snappy")
    print("✅ Transformation complete!")

# parse_health_data("export.xml")

Step 2 – 비밀 소스: DuckDB 로직

Pandas로 전체 Parquet 파일을 RAM에 로드하는 대신, DuckDB를 사용해 직접 쿼리합니다. 이를 통해 복잡한 집계(예: 심박 변동성 대 수면 품질)를 밀리초 단위로 수행할 수 있습니다.

import duckdb
import pandas as pd

def get_heart_rate_summary(parquet_path: str = "health_data.parquet") -> pd.DataFrame:
    """Return daily average & max heart‑rate from the Parquet file."""
    con = duckdb.connect(database=":memory:")
    query = f"""
        SELECT
            date_trunc('day', startDate) AS day,
            AVG(value) AS avg_heart_rate,
            MAX(value) AS max_heart_rate
        FROM '{parquet_path}'
        WHERE type = 'HKQuantityTypeIdentifierHeartRate'
        GROUP BY 1
        ORDER BY 1 DESC
    """
    return con.execute(query).df()

3단계 – Streamlit UI 구축 🎨

Streamlit은 이러한 SQL 결과를 시각화하는 일을 매우 쉽게 만들어 줍니다. 몇 줄의 코드만으로 슬라이더, 날짜 선택기, 인터랙티브 차트를 추가할 수 있습니다.

import streamlit as st
import plotly.express as px

# ----------------------------------------------------------------------
# Page configuration
# ----------------------------------------------------------------------
st.set_page_config(page_title="Quantified Self Dashboard", layout="wide")

st.title("🏃‍♂️ My Quantified Self Dashboard")
st.markdown(
    "Analyzing millions of health records with **DuckDB** speed."
)

# ----------------------------------------------------------------------
# Load data
# ----------------------------------------------------------------------
df_hr = get_heart_rate_summary()

# ----------------------------------------------------------------------
# Layout
# ----------------------------------------------------------------------
col1, col2 = st.columns(2)

with col1:
    st.subheader("Heart Rate Trends")
    fig = px.line(
        df_hr,
        x="day",
        y="avg_heart_rate",
        title="Average Daily Heart Rate",
        markers=True,
    )
    st.plotly_chart(fig, use_container_width=True)

with col2:
    st.subheader("Raw DuckDB Query Speed")
    st.code(
        """SELECT avg(value) FROM 'health_data.parquet' 
WHERE type = 'HKQuantityTypeIdentifierHeartRate'"""
    )
    st.success("Query executed in ~0.002 s")

앱을 실행하려면:

streamlit run your_script.py

공식적인 확장 방법 🥑

로컬에서 개발하는 것은 재미있지만, 프로덕션 수준의 데이터 엔지니어링은 보다 견고한 패턴을 필요로 합니다: 다중 사용자 환경, 자동 데이터 수집, 그리고 고급 머신러닝 파이프라인 등. 이러한 주제에 대해 깊이 있게 알아보려면 WellAlly Blog 를 확인하세요. 이 블로그는 훌륭한 아키텍처 패턴과 프로덕션에 바로 적용 가능한 예시들을 제공하며, 로컬 스크립트를 훨씬 뛰어넘는 내용을 담고 있습니다.

결론

원시 XML 파싱에서 DuckDB + Parquet 워크플로우로 전환함으로써, 느린 데이터 문제를 고성능 분석 도구로 바꾸었습니다. 수백만 건의 건강 기록을 분석하기 위해 거대한 클러스터가 필요하지 않습니다—노트북 하나와 몇 개의 파이썬 라이브러리, 그리고 약간의 호기심만 있으면 됩니다. 즐거운 데이터 분석 되세요!

무엇을 추적하고 있나요?
걸음 수, 수면, 코딩 시간 등, 여러분이 어떻게 삶을 시각화하고 있는지 댓글로 알려주세요! 👇

Back to Blog

관련 글

더 보기 »