feat: add German number formatting functions and enhance narrative context in vital signs insights
- Introduced `_de_num` and `_de_num_signed` functions for formatting decimal numbers with a comma, improving text presentation in German. - Updated `_build_consolidated_paragraphs` to utilize new formatting functions for HRV and resting heart rate comparisons, enhancing clarity in insights. - Refined narrative descriptions for better contextual understanding of vital signs trends and their implications.
This commit is contained in:
parent
8cb5ad992f
commit
ce84f330f0
|
|
@ -83,6 +83,16 @@ def _trailing_window_means(vals: List[float], window: int = 7) -> List[float]:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _de_num(x: float) -> str:
|
||||||
|
"""Dezimalzahl mit Komma für Fließtext."""
|
||||||
|
return f"{x:.1f}".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
|
def _de_num_signed(x: float) -> str:
|
||||||
|
"""Wie _de_num, mit explizitem Vorzeichen (für %-Abweichungen)."""
|
||||||
|
return f"{x:+.1f}".replace(".", ",")
|
||||||
|
|
||||||
|
|
||||||
def _build_consolidated_paragraphs(
|
def _build_consolidated_paragraphs(
|
||||||
series: Dict[str, Any],
|
series: Dict[str, Any],
|
||||||
hrv_vs_baseline_pct: Optional[float],
|
hrv_vs_baseline_pct: Optional[float],
|
||||||
|
|
@ -90,37 +100,33 @@ def _build_consolidated_paragraphs(
|
||||||
r_pearson: Optional[float],
|
r_pearson: Optional[float],
|
||||||
pairs_n: int,
|
pairs_n: int,
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Eine zusammenhängende Einordnung statt vieler einzelner Karten zu Puls/HRV/Basis."""
|
"""
|
||||||
|
Thematisch zusammengeführte Absätze — inhaltlich alle früheren Einzel-Karten (Bullets),
|
||||||
|
ohne die Aussagen zu streichen (Redundanz nur bei wörtlicher Doppelung vermeiden).
|
||||||
|
"""
|
||||||
paras: List[str] = []
|
paras: List[str] = []
|
||||||
|
|
||||||
|
# ── Referenzlage (HRV/Ruhepuls vs. ältere Basis), wie zuvor in KPI/Narrativ genutzt
|
||||||
basis_bits: List[str] = []
|
basis_bits: List[str] = []
|
||||||
if hrv_vs_baseline_pct is not None:
|
if hrv_vs_baseline_pct is not None:
|
||||||
basis_bits.append(
|
basis_bits.append(
|
||||||
f"HRV liegt gegenüber der älteren Referenz bei {hrv_vs_baseline_pct:+.1f} %".replace(".", ",")
|
f"HRV liegt gegenüber der älteren Referenz bei {_de_num_signed(float(hrv_vs_baseline_pct))} %"
|
||||||
)
|
)
|
||||||
if rhr_vs_baseline_pct is not None:
|
if rhr_vs_baseline_pct is not None:
|
||||||
basis_bits.append(
|
basis_bits.append(
|
||||||
f"Ruhepuls relativ zur Referenz bei {rhr_vs_baseline_pct:+.1f} %".replace(".", ",")
|
f"Ruhepuls relativ zur Referenz bei {_de_num_signed(float(rhr_vs_baseline_pct))} %"
|
||||||
)
|
)
|
||||||
if basis_bits:
|
if basis_bits:
|
||||||
paras.append(
|
paras.append(
|
||||||
" ".join(basis_bits)
|
" ".join(basis_bits)
|
||||||
+ " (Vergleich kurzfristiges Mittel vs. ältere Basis — individuell interpretieren)."
|
+ " — Vergleich kurzfristiges Mittel gegenüber älterer Basis; individuell interpretieren."
|
||||||
)
|
)
|
||||||
|
|
||||||
rhr = series.get("resting_hr")
|
rhr = series.get("resting_hr")
|
||||||
hrv_s = series.get("hrv")
|
hrv_s = series.get("hrv")
|
||||||
var_bits: List[str] = []
|
|
||||||
if rhr and rhr.get("stdev") is not None and rhr.get("n", 0) >= 3:
|
|
||||||
var_bits.append(f"Ruhepuls Schwankungsbreite im Fenster etwa σ = {rhr['stdev']} bpm")
|
|
||||||
if hrv_s and hrv_s.get("stdev") is not None and hrv_s.get("n", 0) >= 3:
|
|
||||||
var_bits.append(f"HRV etwa σ = {hrv_s['stdev']} ms")
|
|
||||||
if var_bits:
|
|
||||||
paras.append(
|
|
||||||
"Einzelwerte können stark springen; die gestrichelte Linie zeigt einen gleitenden Mittelwert (max. 7 Messungen). "
|
|
||||||
+ "Im Fenster: " + "; ".join(var_bits) + "."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# ── Ruhepuls: letzte 7 Messungen vs. vorangehendes Fenster (wie frühere Karten)
|
||||||
|
rhr_short_compare = ""
|
||||||
if rhr and rhr.get("points") and len(rhr["points"]) >= 10:
|
if rhr and rhr.get("points") and len(rhr["points"]) >= 10:
|
||||||
pts = rhr["points"]
|
pts = rhr["points"]
|
||||||
last7 = [p["value"] for p in pts[-7:]]
|
last7 = [p["value"] for p in pts[-7:]]
|
||||||
|
|
@ -129,34 +135,86 @@ def _build_consolidated_paragraphs(
|
||||||
m7 = statistics.mean(last7)
|
m7 = statistics.mean(last7)
|
||||||
mb = statistics.mean(before)
|
mb = statistics.mean(before)
|
||||||
diff = m7 - mb
|
diff = m7 - mb
|
||||||
if abs(diff) > 3:
|
if diff > 3:
|
||||||
paras.append(
|
rhr_short_compare = (
|
||||||
f"Kurzfristig liegt der Ruhepuls im Mittel der letzten 7 Messungen "
|
f"Die letzten 7 Messungen liegen im Mittel ca. {_de_num(diff)} bpm über dem vorangehenden Fenster — "
|
||||||
f"{'über' if diff > 0 else 'unter'} dem vorherigen Fenster (Δ ca. {abs(diff):.1f} bpm) — Kontext: Belastung, Schlaf, Stress."
|
"kann mit Belastung, Stress, Schlaf oder Infekt zusammenhängen."
|
||||||
|
)
|
||||||
|
elif diff < -3:
|
||||||
|
rhr_short_compare = (
|
||||||
|
"Der Ruhepuls liegt im kurzen Vergleich unter dem vorherigen Mittel — oft mit Entlastung oder "
|
||||||
|
"besserer Regeneration vereinbar (individuell)."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Streuung: frühere Schwellen n ≥ 6 für die ausführlichen Varianz-Hinweise
|
||||||
|
rhr_var_sentence = ""
|
||||||
|
if (
|
||||||
|
rhr
|
||||||
|
and rhr.get("stdev") is not None
|
||||||
|
and rhr.get("n", 0) >= 6
|
||||||
|
):
|
||||||
|
rhr_var_sentence = (
|
||||||
|
f"Standardabweichung im Fenster ca. {_de_num(float(rhr['stdev']))} bpm — kurzfristige Schwankungen sind normal; "
|
||||||
|
"extreme Sprünge mit Kontext (Training, Schlaf) betrachten."
|
||||||
|
)
|
||||||
|
|
||||||
|
hrv_var_sentence = ""
|
||||||
|
if (
|
||||||
|
hrv_s
|
||||||
|
and hrv_s.get("stdev") is not None
|
||||||
|
and hrv_s.get("n", 0) >= 6
|
||||||
|
):
|
||||||
|
hrv_var_sentence = (
|
||||||
|
f"HRV schwankt im Fenster (σ ≈ {_de_num(float(hrv_s['stdev']))} ms). "
|
||||||
|
"Vergleich mit der eigenen Basis ist aussagekräftiger als Einzelwerte."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gestrichelte Linie = gleitender Mittelwert (neuer Kontext, ergänzt nicht ersetzt)
|
||||||
|
ma_hint = (
|
||||||
|
"Einzelwerte können stark springen; die gestrichelte Linie im Diagramm zeigt einen gleitenden Mittelwert "
|
||||||
|
"über bis zu sieben aufeinanderfolgende Messungen (nicht Kalendertage)."
|
||||||
|
)
|
||||||
|
|
||||||
|
block_b_parts: List[str] = []
|
||||||
|
if rhr_short_compare:
|
||||||
|
block_b_parts.append(rhr_short_compare)
|
||||||
|
if rhr_var_sentence:
|
||||||
|
block_b_parts.append(rhr_var_sentence)
|
||||||
|
if hrv_var_sentence:
|
||||||
|
block_b_parts.append(hrv_var_sentence)
|
||||||
|
if block_b_parts:
|
||||||
|
paras.append(ma_hint + " " + " ".join(block_b_parts))
|
||||||
|
elif series:
|
||||||
|
# Kein Kurzvergleich/keine σ-Sätze, aber mindestens eine Vital-Zeitreihe: MA-Hinweis (Diagramm)
|
||||||
|
paras.append(ma_hint)
|
||||||
|
|
||||||
|
# ── VO2max: Wortlaut wie in den früheren Bullet-Karten
|
||||||
vo2 = series.get("vo2_max")
|
vo2 = series.get("vo2_max")
|
||||||
if vo2 and vo2.get("n", 0) >= 4 and vo2.get("slope_per_day") is not None:
|
if vo2 and vo2.get("n", 0) >= 4 and vo2.get("slope_per_day") is not None:
|
||||||
s = vo2["slope_per_day"]
|
s = vo2["slope_per_day"]
|
||||||
if s > 0.002:
|
if s > 0.002:
|
||||||
paras.append(
|
paras.append(
|
||||||
"VO2max steigt im gewählten Fenster tendenziell — oft mit Trainingsreiz oder stabilen Messungen vereinbar."
|
"Im gewählten Fenster steigt der erfasste VO2max tendenziell — häufig mit Trainingsreiz oder "
|
||||||
|
"besserer Datenlage vereinbar."
|
||||||
)
|
)
|
||||||
elif s < -0.002:
|
elif s < -0.002:
|
||||||
paras.append(
|
paras.append(
|
||||||
"VO2max fällt im Fenster leicht — kann Pause, Krankheit oder Messrauschen widerspiegeln."
|
"VO2max zeigt im Fenster einen fallenden Trend — kann z. B. durch Pause, Krankheit oder Messrauschen "
|
||||||
|
"entstehen; Verlauf beobachten."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Belastung vs. Folge-Ruhepuls: frühere Formulierungen + r/n wo berechnet
|
||||||
if r_pearson is not None and pairs_n >= 8:
|
if r_pearson is not None and pairs_n >= 8:
|
||||||
if r_pearson > 0.35:
|
if r_pearson > 0.35:
|
||||||
paras.append(
|
paras.append(
|
||||||
f"Korrelation Trainingsminuten (Tag) → Ruhepuls (Folgetag): r ≈ {r_pearson:.2f} (n = {pairs_n} Paare). "
|
"An Tagen nach höherer Trainingsdauer (Minuten-Summe) steigt der Ruhepuls am nächsten Morgen in deinen "
|
||||||
"Höhere Belastung und etwas höherer Ruhepuls am nächsten Morgen kommen in den Daten häufig zusammen — kein Kausalbeweis."
|
"Daten tendenziell — typisches Muster während Erholungsreaktion (kein Kausalbeweis). "
|
||||||
|
f"Korrelation (Trainingsminuten am Tag → Ruhepuls am Folgetag): r ≈ {r_pearson:.2f} bei n = {pairs_n} Paaren."
|
||||||
)
|
)
|
||||||
elif r_pearson < -0.25:
|
elif r_pearson < -0.25:
|
||||||
paras.append(
|
paras.append(
|
||||||
f"Zwischen Tages-Belastung und Folge-Ruhepuls zeigt sich ein leicht negatives Zusammenspiel (r ≈ {r_pearson:.2f}, n = {pairs_n}). "
|
"Es zeigt sich ein leicht negatives Zusammenspiel zwischen Tages-Belastung und Folge-Ruhepuls in diesem "
|
||||||
"Stark von Ausreißern und Datenlücken abhängig."
|
f"Fenster — stark von Datenlage und Ausreißern abhängig. r ≈ {r_pearson:.2f}, n = {pairs_n} Paare."
|
||||||
)
|
)
|
||||||
|
|
||||||
return [p for p in paras if p]
|
return [p for p in paras if p]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user