added getting data from nightscout

This commit is contained in:
2026-07-25 08:54:08 +02:00
parent c02110a82b
commit 60735de445
12 changed files with 613 additions and 285 deletions
+42 -32
View File
@@ -3,50 +3,60 @@ Data processing logic voor het glucose-dashboard.
Bewust gescheiden van app.py: Streamlit-UI-code kun je niet zomaar los
unit-testen (die heeft een draaiende Streamlit-context nodig). Door de
logica (inladen, aggregeren) in een los module te zetten, kun je die
logica (classificeren, samenvatten) in een los module te zetten, kun je die
logica wel gewoon met pytest testen, en blijft app.py alleen de UI-laag.
"""
from pathlib import Path
from __future__ import annotations
import pandas as pd
DATA_PATH = Path(__file__).parent / "data" / "glucose_readings.csv"
from nightscout_client import SENSOR_CHANGE_EVENT_TYPES, SITE_CHANGE_EVENT_TYPES
# Streefbereik in mmol/L — pas aan naar je eigen behandeldoelen.
TARGET_LOW = 3.9
TARGET_HIGH = 10.0
def load_data(path: Path = DATA_PATH) -> pd.DataFrame:
"""Laad de ruwe metingen in.
def classify_treatments(treatments: pd.DataFrame) -> dict[str, pd.DataFrame]:
"""Splits ruwe Nightscout-treatments op in aparte, tijd-gesorteerde subsets.
Verwacht kolommen: 'timestamp' (parsebaar als datetime) en
'glucose_value' (numeriek). Vervang deze functie later door je
eigen databron (database, CGM-export, API-call, etc.) — de rest
van de app blijft dan ongewijzigd werken.
Een insuline- en koolhydraat-registratie kunnen dezelfde treatment zijn
(bijv. een "Meal Bolus" met zowel insuline als carbs) en verschijnen dan
in beide subsets.
"""
df = pd.read_csv(path, parse_dates=["timestamp"])
return df
insulin = treatments[treatments["insulin"].fillna(0) > 0]
carbs = treatments[treatments["carbs"].fillna(0) > 0]
site_changes = treatments[treatments["eventType"].isin(SITE_CHANGE_EVENT_TYPES)]
sensor_changes = treatments[treatments["eventType"].isin(SENSOR_CHANGE_EVENT_TYPES)]
return {
"insulin": insulin.reset_index(drop=True),
"carbs": carbs.reset_index(drop=True),
"site_changes": site_changes.reset_index(drop=True),
"sensor_changes": sensor_changes.reset_index(drop=True),
}
def filter_by_date(df: pd.DataFrame, selected_date) -> pd.DataFrame:
"""Filter de dataset op een enkele kalenderdag."""
return df[df["timestamp"].dt.date == selected_date]
def glucose_status(mmol: float, low: float = TARGET_LOW, high: float = TARGET_HIGH) -> str:
"""Classificeer een glucosewaarde t.o.v. het streefbereik."""
if mmol < low:
return "low"
if mmol > high:
return "high"
return "in_range"
def hourly_average(df: pd.DataFrame) -> pd.DataFrame:
"""Groepeer per uur (0-23) en bereken de gemiddelde waarde.
def summarize_time_in_range(
glucose: pd.DataFrame, low: float = TARGET_LOW, high: float = TARGET_HIGH
) -> dict[str, float]:
"""Bereken het percentage metingen onder/in/boven het streefbereik."""
if glucose.empty:
return {"low": 0.0, "in_range": 0.0, "high": 0.0}
Geeft een DataFrame terug met kolommen 'hour' en 'glucose_value',
één rij per uur waarin daadwerkelijk data zit.
"""
if df.empty:
return pd.DataFrame(columns=["hour", "glucose_value"])
working = df.copy()
working["hour"] = working["timestamp"].dt.hour
result = (
working.groupby("hour")["glucose_value"]
.mean()
.round(1)
.reset_index()
.sort_values("hour")
)
return result
statuses = glucose["mmol"].apply(lambda v: glucose_status(v, low, high))
counts = statuses.value_counts(normalize=True) * 100
return {
"low": round(counts.get("low", 0.0), 1),
"in_range": round(counts.get("in_range", 0.0), 1),
"high": round(counts.get("high", 0.0), 1),
}