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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user