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