4 min read

Health Daten Auswertung

Ich habe gestern bei meinem großen 10 Km Spaziergang darüber nachgedacht, was denn eigentlich mit all den aggregierten Daten ist, die ich seit Jahren ansammle. Und dabei meine ich die Gesundheitsdaten in Apple Health.

Apple Health sammelt eine riesige Bandbreite an Gesundheits- und Fitnessdaten wie 

  • Aktivität (Schritte, Training), 
  • Schlaf, Ernährung, Herzgesundheit (Ruhepuls, EKG), Vitalwerte (Blutdruck, Blutsauerstoff), 
  • Körpermaße (Gewicht, BMI), 
  • Mentale Gesundheit (Achtsamkeit, Umgebungslärm)

und mehr, zentralisiert diese Daten auf dem iPhone, zieht weitere Daten von Apple Watch oder Drittanbieter-Apps/Geräten hinzu und ermöglicht so die Analyse über Zeiträume sowie den Export dieser aggregierten Daten.

Und an genau diesen Export habe ich dabei gedacht und mir überlegt, ob man aus den Daten nicht die Bewegungsdaten pro Tag in eine (z.B.) JSON-Datei extrahieren und diese JSON dann wieder mit etwas Javascript auf meiner Website auswerten und visualisieren könnte?

Gesagt, getan. Ich habe also die Daten aus der Apple Health App exportiert. Es entsteht eine Export.zip. Nun habe ich mit einem Python-Script:

import xml.etree.ElementTree as ET
from datetime import datetime
import json

def parse_apple_health_robust(xml_path, target_source=None):
    tree = ET.parse(xml_path)
    root = tree.getroot()
    daily_totals_km = {}
    processed_count = 0
    skipped_count = 0

    for record in root.findall('Record'):
        if record.get('type') == 'HKQuantityTypeIdentifierDistanceWalkingRunning':
            source = record.get('sourceName', '')
            # Optional: Nach Quelle filtern (falls gewünscht)
            if target_source and (target_source not in source):
                skipped_count += 1
                continue
[…] Das ist nur der Anfang, das vollständige Script findet ihr [hier](https://paste.janmontag.de/upload/eel-pigeon-dog)

(Link zum vollständigen Python-Script)

die Export.xml aus dem Apple Health Export zerlegt und eine Datei walking_data.json erstellt. Diese Datei habe ich auf meinen Webserver geladen. Sie beinhaltet ungefähr folgendes:

{
  "daily": {
    "2023-04-07": 0.002425179999999999,
    "2023-02-19": 0.005502,
    "2022-12-31": 0.0149992,
    "2023-01-01": 0.008363970000000002,
    "2022-08-21": 0.00085309,
    "2022-08-22": 0.0009541399999999999,
    "2022-05-03": 0.00224689,
    "2022-05-01": 0.019163234,
    "2022-05-04": 0.002758355,
    "2022-11-25": 0.0033183299999999995,
    "2022-11-26": 0.007765545000000001,
    "2022-11-27": 0.00259157,
    "2022-11-28": 0.007173079999999998,
    "2022-11-29": 0.007171858999999999,
    …
    }
}

Also grob gesagt das Datum und die zurück gelegten Kilometer.

Und dann habe ich ein Ghost-Custom-Template (hbs) mit dem Namen custom-health.hbs erstellt. Im Backend habe ich eine neue Page angelegt und dieses Template ausgewählt.

{{!< default}}

<main class="gh-main">
  <div class="gh-outer">
    <div class="gh-inner">
      <article class="gh-canvas">

        <header class="gh-article-header">
          <h5 class="gh-article-title">Bewegung</h5>
            <p>Gelaufene Kilometer pro Tag – aggregiert aus Apple Health.</p>
        </header>

        <section class="gh-content">
          <h5>Wöchentliche Übersicht</h5>
          <div id="weekly-chart" style="height:300px; max-width:100%;"></div>

          <h5>Jahresübersicht</h5>
          <div id="yearly-chart" style="height:300px; max-width:100%;"></div>
        </section>

      </article>
    </div>
  </div>
</main>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{asset "js/Charts.js"}}"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script> -->

(Link zur vollständigen Datei)

Die Datei /assets/js/Charts.js habe ich wenig später angelegt und dort den folgenden Javascript-Auswertungscode (Paste) implementiert.

(async function () {
    console.log('Charts.js loaded');
  
    const DATA_URL = '/health-data/walking_data.json';
    const WEEK_CONTAINER_ID = 'weekly-chart';
    const YEAR_CONTAINER_ID = 'yearly-chart';
  
    try {
      const response = await fetch(DATA_URL);
      if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
      const rawData = await response.json();
  
      if (typeof rawData !== 'object' || Array.isArray(rawData)) throw new Error('Unexpected JSON structure');
  
      const entries = Object.entries(rawData)
        .filter(([date, km]) => typeof km === 'number' && km >= 0)
        .sort(([a], [b]) => a.localeCompare(b));
  
      if (entries.length === 0) throw new Error('No data points found');
  
      // ========================
      // Hilfsfunktionen
      // ========================
      function getWeekStart(date) {
        const d = new Date(date);
        const day = d.getDay();
        d.setDate(d.getDate() - (day === 0 ? 6 : day - 1)); // Montag
        return d;
      }
  
      function getWeekDates(startDate) {
        const dates = [];
        for (let i = 0; i < 7; i++) {
          const d = new Date(startDate);
          d.setDate(d.getDate() + i);
          dates.push(d.toISOString().slice(0, 10));
        }
        return dates;
      }
  
      function getWeekData(startDate) {
        return getWeekDates(startDate).map(d => rawData[d] || 0);
      }
  
      // ========================
      // Wochen-Chart
      // ========================
      let latestDate = entries[entries.length - 1][0];
      let currentWeekStart = getWeekStart(latestDate);
  
      const weeklyContainer = document.getElementById(WEEK_CONTAINER_ID);
      if (!weeklyContainer) throw new Error(`#${WEEK_CONTAINER_ID} not found`);
      weeklyContainer.innerHTML = ''; // clean container
  
[…] Das ist nur ein Auszug asu dem Javascript, die volle Datei findet ihr [hier](https://paste.janmontag.de/upload/sloth-goose-sheep).
  

(Link zum vollständigen Javascript Code)

Später muste ich noch eine Möglichkeit schaffen, dass meine JSON Datei durch das Javascript gelesen und ausgewertet werden kann. Das habe ich mit Caddy umgesetzt, dies ist für jeden Webserver verschieden.

💡
Start: Apple Health Export --> Python Script extrahiert nur die Bewegungsdaten ohne GPS etc. pp. und speichert diese in einer JSON Datei --> JSON-Datei wird auf den Webserver übertragen --> janmontag.de/health ruft über ein Custom Template das Javascript Charts.jsauf, die wiederum die extrahierte JSON Datei in Graphen darstellt.

Es ist und bleibt also explizit ein händischer Akt, die Daten aus dem iPhone zu exportieren. Das ist so gewollt, eine API oder Schnittstelle wäre bei so heiklen Daten wie den aggregierten Apple Health Gesundheitsdaten ein nicht zu kalkulierendes Daten/Sicherheitsrisiko. Und mal ehrlich, ich brauche die Daten nicht zeitnah. Ich hatte einfach nur Interesse meine Bewegungen darzustellen und vielleicht motiviert mich das ja, noch aktiver zu werden.

Und so sieht das ganze am Ende aus:

Darstellung meiner Health Bewegunsdaten nach Tagen und Jahren
Darstellung meiner Health Bewegunsdaten nach Tagen und Jahren
💡
Hier findet ihr meine Walking-Stats.

Reaktionen

Lade Interaktionen...