mitai-jinkendo/backend/data_layer/vital_signs_assessment.py

157 lines
5.8 KiB
Python
Raw Normal View History

"""
Orientierende Zonen-Einschätzungen für Vitalwerte (Layer 1, Issue 53).
Keine Diagnose — typische Referenzbereiche für UI/Coaching.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Set
from data_layer.utils import safe_float
Tone = str # good | warn | bad | neutral
def _item(
key: str,
label_de: str,
value_display: str,
tone: Tone,
zone_label_de: str,
hint_de: str,
sort_order: int,
) -> Dict[str, Any]:
return {
"key": key,
"label_de": label_de,
"value_display": value_display,
"tone": tone,
"zone_label_de": zone_label_de,
"hint_de": hint_de,
"sort_order": sort_order,
}
def assess_resting_hr(bpm: float) -> tuple:
if bpm < 50:
return (
"warn",
"Niedrig",
"Unter 50 bpm kann bei Sportlern normal sein — sonst ärztlich klären, wenn neu oder mit Beschwerden.",
)
if bpm < 60:
return ("good", "Günstig / athletisch", "Häufig bei gut trainierten Personen im unteren Normbereich.")
if bpm <= 100:
return ("good", "Im üblichen Normbereich", "Typischer Ruhepuls bei Erwachsenen oft ca. 60–100 bpm.")
if bpm <= 110:
return ("warn", "Leicht erhöht", "Kann durch Stress, Krankheit, Koffein oder Untrainiertheit erhöht sein — Verlauf beobachten.")
return ("bad", "Deutlich erhöht", "Bei anhaltend hohem Ruhepuls medizinische Abklärung sinnvoll.")
def assess_hrv_ms(ms: float) -> tuple:
_ = ms
return (
"neutral",
"Individuell",
"HRV (ms) ist sehr personenabhängig; Aussagekraft vor allem im Vergleich zu deiner eigenen Basis/Trend.",
)
def assess_blood_pressure(systolic: float, diastolic: float) -> tuple:
sys_, dia = systolic, diastolic
if sys_ >= 180 or dia >= 110:
return ("bad", "Sehr hoch", "Sehr hohe Werte — bei Beschwerden oder neu aufgetreten ärztlich zeitnah abklären.")
if sys_ >= 140 or dia >= 90:
return (
"bad",
"Erhöht",
"Liegt in einem Bereich, der oft als Hypertonie eingestuft wird — Bestätigung und Beratung durch ärztliche Messung.",
)
if sys_ >= 130 or dia >= 85:
return ("warn", "Hochnormal", "Oberer Normal-/hochnormaler Bereich — Lebensstil und Verlauf beachten.")
if sys_ < 120 and dia < 80:
return ("good", "Optimal", "Liegt in einem oft als günstig beschriebenen Bereich (<120/80 mmHg).")
return ("good", "Normal", "Im gängigen Zielbereich für viele Erwachsene.")
def assess_spo2(pct: float) -> tuple:
if pct >= 97:
return ("good", "Günstig", "Sauerstoffsättigung im üblichen Zielbereich.")
if pct >= 95:
return ("good", "Unauffällig", "Häufig noch als normal eingestuft; Verlauf bei Atembeschwerden beobachten.")
if pct >= 90:
return ("warn", "Leicht vermindert", "Unter 95 % kann je nach Kontext relevant sein — bei Symptomen abklären.")
return ("bad", "Niedrig", "Niedrige SpO2 — bei anhaltend unter 90 % oder Beschwerden ärztlich vorstellen.")
def assess_respiratory_rate(rpm: float) -> tuple:
if 12 <= rpm <= 20:
return ("good", "Im üblichen Bereich", "Ruheatmung oft ca. 12–20/min.")
if 10 <= rpm < 12 or 20 < rpm <= 24:
return ("warn", "Grenzbereich", "Leicht außerhalb des häufig zitierten Ruhebereichs — Kontext (Belastung, Stress) beachten.")
return ("bad", "Auffällig", "Deutlich außerhalb typischer Ruhewerte — bei Beschwerden medizinisch abklären.")
def assess_vo2_max(value: float) -> tuple:
_ = value
return (
"neutral",
"Orientativ",
"VO2max hängt stark von Alter, Geschlecht und Messmethode ab; Trends in der App sind aussagekräftiger als Einzelwerte.",
)
def build_vital_items_from_rows(
vitals_row: Optional[Dict[str, Any]],
bp_row: Optional[Dict[str, Any]],
omit_keys: Optional[Set[str]] = None,
) -> List[Dict[str, Any]]:
"""omit_keys: z. B. {'resting_hr','hrv'} wenn Einordnung zentral im Herz-/Autonomie-Block steht."""
skip = omit_keys or set()
items: List[Dict[str, Any]] = []
order = 0
if vitals_row:
rhr = vitals_row.get("resting_hr")
if rhr is not None and "resting_hr" not in skip:
v = safe_float(rhr)
t, z, h = assess_resting_hr(v)
items.append(_item("resting_hr", "Ruhepuls", f"{v:.0f} bpm", t, z, h, order))
order += 1
hrv = vitals_row.get("hrv")
if hrv is not None and "hrv" not in skip:
v = safe_float(hrv)
t, z, h = assess_hrv_ms(v)
items.append(_item("hrv", "HRV", f"{v:.0f} ms", t, z, h, order))
order += 1
vo2 = vitals_row.get("vo2_max")
if vo2 is not None:
v = safe_float(vo2)
t, z, h = assess_vo2_max(v)
items.append(_item("vo2_max", "VO2max", f"{v:.1f} ml/kg/min", t, z, h, order))
order += 1
spo2 = vitals_row.get("spo2")
if spo2 is not None:
v = safe_float(spo2)
t, z, h = assess_spo2(v)
items.append(_item("spo2", "SpO2", f"{v:.0f} %", t, z, h, order))
order += 1
rr = vitals_row.get("respiratory_rate")
if rr is not None:
v = safe_float(rr)
t, z, h = assess_respiratory_rate(v)
items.append(_item("respiratory_rate", "Atemfrequenz", f"{v:.0f} /min", t, z, h, order))
order += 1
if bp_row and bp_row.get("systolic") is not None and bp_row.get("diastolic") is not None:
sys_v = safe_float(bp_row["systolic"])
dia_v = safe_float(bp_row["diastolic"])
t, z, h = assess_blood_pressure(sys_v, dia_v)
items.append(_item("blood_pressure", "Blutdruck", f"{sys_v:.0f}/{dia_v:.0f} mmHg", t, z, h, order))
return items