add Trends page: GMI trend, overnight/daytime, weekday pattern, bolus, site-age (FR7, FR8)

reporting.py gains overnight_daytime_split, day_of_week_pattern,
bolus_stats, and site_interval_stats. The last two are explicitly scoped
to bolus-only insulin (no basal reconstruction) since that needs the
Nightscout profile, which is the separate profile-tuning feature's
territory — documented as a caveat in both the UI and the design doc.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-26 21:06:06 +02:00
co-authored by Claude Sonnet 5
parent 04c5f521fe
commit 9929f44daa
3 changed files with 363 additions and 0 deletions
+112
View File
@@ -3,11 +3,15 @@ import pytest
from reporting import (
agp_percentile_bands,
bolus_stats,
day_of_week_pattern,
detect_events,
gmi_percent,
minutes_per_day,
overnight_daytime_split,
percent_cv,
sensor_active_percent,
site_interval_stats,
summarize_events,
tir_breakdown_5band,
)
@@ -243,6 +247,114 @@ def test_agp_percentile_bands_handles_empty_input():
assert list(result.columns) == ["minute_of_day", "p5", "p25", "p50", "p75", "p95"]
def test_overnight_daytime_split_separates_by_hour():
glucose = _series(
[
("2026-01-01 02:00", 3.0), # nacht, laag
("2026-01-01 03:00", 3.0), # nacht, laag
("2026-01-01 12:00", 6.0), # dag, in bereik
("2026-01-01 13:00", 6.0), # dag, in bereik
]
)
result = overnight_daytime_split(glucose, overnight_start=0, overnight_end=6)
assert result["overnight"]["low"] == pytest.approx(100.0)
assert result["daytime"]["in_range"] == pytest.approx(100.0)
def test_overnight_daytime_split_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
result = overnight_daytime_split(empty)
assert result["overnight"] == result["daytime"]
def test_day_of_week_pattern_computes_per_weekday_stats():
# 2026-01-05 is een maandag
glucose = _series(
[
("2026-01-05 08:00", 6.0),
("2026-01-05 09:00", 8.0),
("2026-01-12 08:00", 7.0), # ook een maandag
]
)
result = day_of_week_pattern(glucose)
monday = result[result["weekday"] == "Maandag"].iloc[0]
assert monday["count"] == 3
assert monday["mean_mmol"] == pytest.approx(7.0)
tuesday = result[result["weekday"] == "Dinsdag"].iloc[0]
assert tuesday["count"] == 0
assert pd.isna(tuesday["mean_mmol"])
def test_day_of_week_pattern_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
result = day_of_week_pattern(empty)
assert result.empty
assert list(result.columns) == ["weekday", "mean_mmol", "in_range_pct", "count"]
def test_bolus_stats_splits_by_event_type():
treatments = pd.DataFrame(
{
"timestamp": pd.to_datetime(["2026-01-01 08:00", "2026-01-01 12:00", "2026-01-01 20:00"]),
"eventType": ["Meal Bolus", "Correction Bolus", "Site Change"],
"insulin": [4.0, 1.5, None],
"carbs": [45.0, None, None],
}
)
result = bolus_stats(treatments)
assert result["total_units"] == pytest.approx(5.5)
assert result["count"] == 2
assert result["by_type"] == {"Meal Bolus": 4.0, "Correction Bolus": 1.5}
def test_bolus_stats_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "eventType", "insulin", "carbs"])
result = bolus_stats(empty)
assert result == {"total_units": 0.0, "count": 0, "by_type": {}}
def test_site_interval_stats_splits_by_site_change():
glucose = _series(
[
("2026-01-01 00:00", 6.0),
("2026-01-05 00:00", 6.0),
("2026-01-10 00:00", 8.0),
]
)
treatments = pd.DataFrame(
{
"timestamp": pd.to_datetime(["2026-01-02 00:00", "2026-01-06 00:00"]),
"eventType": ["Meal Bolus", "Meal Bolus"],
"insulin": [3.0, 4.0],
"carbs": [30.0, 40.0],
}
)
site_changes = pd.DataFrame(
{
"timestamp": pd.to_datetime(["2026-01-01 00:00", "2026-01-06 00:00"]),
"eventType": ["Site Change", "Site Change"],
"insulin": [None, None],
"carbs": [None, None],
}
)
result = site_interval_stats(glucose, treatments, site_changes)
assert len(result) == 2
assert result.iloc[0]["duration_days"] == pytest.approx(5.0)
assert result.iloc[0]["total_bolus_units"] == pytest.approx(3.0)
# de bolus op 6 jan valt binnen het tweede interval ([6 jan, 10 jan))
assert result.iloc[1]["total_bolus_units"] == pytest.approx(4.0)
def test_site_interval_stats_handles_no_site_changes():
glucose = _series([("2026-01-01 00:00", 6.0)])
treatments = pd.DataFrame(columns=["timestamp", "eventType", "insulin", "carbs"])
empty_site_changes = pd.DataFrame(columns=["timestamp", "eventType", "insulin", "carbs"])
result = site_interval_stats(glucose, treatments, empty_site_changes)
assert result.empty
def test_summarize_events_handles_empty_input():
empty = pd.DataFrame(columns=["start", "end", "duration_minutes"])
result = summarize_events(empty, period_days=14)