Replace monthly GMI bar chart with daily scatter + rolling avg + linear trend/CI

Adds daily_gmi() and gmi_trend(): per-day GMI as scatter points, a 7-day
rolling average line, and a linear regression trend line with a 95%
confidence band (normal approximation, no scipy dependency, consistent
with the existing preference for a lean container image).

Recomputed from the already-fetched period data on each page load; the
existing 1-hour Streamlit cache on load_range_data() already avoids
repeated Nightscout calls for the same period, so no separate storage
layer is needed at the current max. 90-day report window.

Closes gitea #19.
This commit is contained in:
2026-08-01 22:57:54 +02:00
parent 9e11dd8d73
commit 374ead70e5
3 changed files with 173 additions and 11 deletions
+54
View File
@@ -4,13 +4,16 @@ import pandas as pd
import pytest
from reporting import (
GMI_TREND_COLUMNS,
agp_percentile_bands,
basal_stats,
bolus_stats,
completeness_warning,
daily_gmi,
day_of_week_pattern,
detect_events,
gmi_percent,
gmi_trend,
minutes_per_day,
overnight_daytime_split,
percent_cv,
@@ -32,6 +35,57 @@ def test_gmi_percent_matches_bergenstal_formula():
assert gmi_percent(7.0) == pytest.approx(6.3, abs=0.05)
def test_daily_gmi_aggregates_per_calendar_day():
glucose = _series(
[
("2026-01-01 08:00", 6.0),
("2026-01-01 20:00", 8.0),
("2026-01-02 08:00", 7.0),
]
)
result = daily_gmi(glucose)
assert list(result["date"].astype(str)) == ["2026-01-01", "2026-01-02"]
assert result["gmi"].iloc[0] == pytest.approx(gmi_percent(7.0)) # gemiddelde van 6.0 en 8.0
assert result["gmi"].iloc[1] == pytest.approx(gmi_percent(7.0))
def test_daily_gmi_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
result = daily_gmi(empty)
assert result.empty
assert list(result.columns) == ["date", "gmi"]
def test_gmi_trend_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
result = gmi_trend(empty)
assert result.empty
assert list(result.columns) == GMI_TREND_COLUMNS
def test_gmi_trend_returns_no_line_for_a_single_day():
glucose = _series([("2026-01-01 08:00", 6.0)])
result = gmi_trend(glucose)
assert list(result.columns) == GMI_TREND_COLUMNS
assert len(result) == 1
assert pd.isna(result["trend"].iloc[0])
def test_gmi_trend_fits_increasing_linear_trend_with_band_around_it():
pairs = [(f"2026-01-{i + 1:02d} 08:00", 6.0 + i * 0.2) for i in range(10)]
glucose = _series(pairs)
result = gmi_trend(glucose, rolling_window_days=3)
assert len(result) == 10
assert result["trend"].iloc[-1] > result["trend"].iloc[0]
assert (result["trend_lower"] <= result["trend"]).all()
assert (result["trend"] <= result["trend_upper"]).all()
# Vrijwel lineaire input (gmi_percent rondt af op 1 decimaal, dus een
# kleine restfout blijft over) -> smalle betrouwbaarheidsband.
assert (result["trend_upper"] - result["trend_lower"]).max() == pytest.approx(0, abs=0.15)
assert result["rolling_avg"].iloc[-1] > result["rolling_avg"].iloc[0]
def test_percent_cv_handles_empty_input():
empty = pd.DataFrame(columns=["timestamp", "mmol"])
assert percent_cv(empty) == 0.0