my.body.blood: prettify, add stat()

This commit is contained in:
Dima Gerasimov 2020-09-01 18:43:37 +01:00 committed by karlicoss
parent efea669a3e
commit 743312a87b
3 changed files with 91 additions and 51 deletions

View file

@ -4,110 +4,126 @@ Blood tracking
from datetime import datetime from datetime import datetime
from typing import Iterable, NamedTuple, Optional from typing import Iterable, NamedTuple, Optional
from itertools import chain
import porg from ..core.common import listify
from ..common import listify from ..core.error import Res, echain
from ..error import Res, echain from ..core.orgmode import parse_org_datetime
from kython.org import parse_org_date
from my.config import blood as config
import pandas as pd # type: ignore import pandas as pd # type: ignore
import porg
from my.config import blood as config
class Entry(NamedTuple): class Entry(NamedTuple):
dt: datetime dt: datetime
ket: Optional[float]=None ketones : Optional[float]=None
glu: Optional[float]=None glucose : Optional[float]=None
vitd: Optional[float]=None vitamin_d : Optional[float]=None
b12: Optional[float]=None vitamin_b12 : Optional[float]=None
hdl: Optional[float]=None hdl : Optional[float]=None
ldl: Optional[float]=None ldl : Optional[float]=None
trig: Optional[float]=None triglycerides: Optional[float]=None
extra: Optional[str]=None source : Optional[str]=None
extra : Optional[str]=None
Result = Res[Entry] Result = Res[Entry]
class ParseError(Exception):
pass
def try_float(s: str) -> Optional[float]: def try_float(s: str) -> Optional[float]:
l = s.split() l = s.split()
if len(l) == 0: if len(l) == 0:
return None return None
# meh. this is to strip away HI/LO? Maybe need extract_float instead or something
x = l[0].strip() x = l[0].strip()
if len(x) == 0: if len(x) == 0:
return None return None
return float(x) return float(x)
def iter_gluc_keto_data() -> Iterable[Result]: def glucose_ketones_data() -> Iterable[Result]:
o = porg.Org.from_file(str(config.blood_log)) o = porg.Org.from_file(str(config.blood_log))
tbl = o.xpath('//table') tbl = o.xpath('//table')
# todo some sort of sql-like interface for org tables might be ideal?
for l in tbl.lines: for l in tbl.lines:
kets = l['ket'].strip() kets = l['ket'].strip()
glus = l['glu'].strip() glus = l['glu'].strip()
extra = l['notes'] extra = l['notes']
dt = parse_org_date(l['datetime']) dt = parse_org_datetime(l['datetime'])
assert isinstance(dt, datetime) try:
ket = try_float(kets) assert isinstance(dt, datetime)
glu = try_float(glus) ket = try_float(kets)
yield Entry( glu = try_float(glus)
dt=dt, except Exception as e:
ket=ket, ex = RuntimeError(f'While parsing {l}')
glu=glu, ex.__cause__ = e
extra=extra, yield ex
) else:
yield Entry(
dt=dt,
ketones=ket,
glucose=glu,
extra=extra,
)
def iter_tests_data() -> Iterable[Result]: def blood_tests_data() -> Iterable[Result]:
o = porg.Org.from_file(str(config.blood_tests_log)) o = porg.Org.from_file(str(config.blood_tests_log))
tbl = o.xpath('//table') tbl = o.xpath('//table')
for d in tbl.lines: for d in tbl.lines:
try: try:
dt = parse_org_date(d['datetime']) dt = parse_org_datetime(d['datetime'])
assert isinstance(dt, datetime) assert isinstance(dt, datetime), dt
# TODO rest
F = lambda n: try_float(d[n]) F = lambda n: try_float(d[n])
yield Entry( yield Entry(
dt=dt, dt=dt,
vitd=F('VD nm/L'), vitamin_d =F('VD nm/L'),
b12 =F('B12 pm/L'), vitamin_b12 =F('B12 pm/L'),
hdl =F('HDL mm/L'), hdl =F('HDL mm/L'),
ldl =F('LDL mm/L'), ldl =F('LDL mm/L'),
trig=F('Trig mm/L'), triglycerides=F('Trig mm/L'),
extra=d['misc'], source =d['source'],
extra =d['notes'],
) )
except Exception as e: except Exception as e:
print(e) ex = RuntimeError(f'While parsing {d}')
yield echain(ParseError(str(d)), e) ex.__cause__ = e
yield ex
def data(): def data() -> Iterable[Result]:
datas = list(chain(iter_gluc_keto_data(), iter_tests_data())) from itertools import chain
return list(sorted(datas, key=lambda d: getattr(d, 'dt', datetime.min))) from ..core.error import sort_res_by
datas = chain(glucose_ketones_data(), blood_tests_data())
return sort_res_by(datas, key=lambda e: e.dt)
@listify(wrapper=pd.DataFrame) def dataframe() -> pd.DataFrame:
def dataframe(): rows = []
for d in data(): for x in data():
if isinstance(d, Exception): if isinstance(x, Exception):
yield {'error': str(d)} # todo use some core helper? this is a pretty common operation
d = {'error': str(x)}
else: else:
yield d._asdict() d = x._asdict()
rows.append(d)
return pd.DataFrame(rows)
def stats():
from ..core import stat
return stat(data)
def test(): def test():

21
my/core/orgmode.py Normal file
View file

@ -0,0 +1,21 @@
"""
Various helpers for reading org-mode data
"""
from datetime import datetime
def parse_org_datetime(s: str) -> datetime:
s = s.strip('[]')
for fmt, cl in [
("%Y-%m-%d %a %H:%M", datetime),
("%Y-%m-%d %H:%M" , datetime),
# todo not sure about these... fallback on 00:00?
# ("%Y-%m-%d %a" , date),
# ("%Y-%m-%d" , date),
]:
try:
return datetime.strptime(s, fmt)
except ValueError:
continue
else:
raise RuntimeError(f"Bad datetime string {s}")

View file

@ -100,7 +100,9 @@ def dataframe() -> DataFrameT:
else: else:
# todo it's change during the day?? dunno if reasonable metric # todo it's change during the day?? dunno if reasonable metric
hrv_change = s.hrv_evening - last.hrv_morning hrv_change = s.hrv_evening - last.hrv_morning
# todo maybe changes need to be handled in a more generic way?
# todo ugh. get rid of hardcoding, just generate the schema automatically
# TODO use 'workdays' provider.... # TODO use 'workdays' provider....
d = { d = {
'date' : dd, 'date' : dd,
@ -116,6 +118,7 @@ def dataframe() -> DataFrameT:
'hrv_morning': s.hrv_morning, 'hrv_morning': s.hrv_morning,
'recovery' : s.recovery, 'recovery' : s.recovery,
'hrv_change' : hrv_change, 'hrv_change' : hrv_change,
'respiratory_rate_avg': s.respiratory_rate_avg,
} }
last = s # meh last = s # meh
dicts.append(d) dicts.append(d)