Health Dashboard, 4. deo: kada readiness prestane da bude jedan broj
Četvrti deo mog build log-a za Health Dashboard: kako je audit pet godina ličnih podataka oborio originalnu readiness formulu, zašto je naivni baseline pobedio svaki linearni model koji sam probao i šta u praksi znači „bez zbirne ocene“.
U 1. delu opisao sam originalni readiness score:
readiness = HRV_score * 0.40 + RHR_score * 0.30 + Sleep_score * 0.30
Bio je to koristan broj. Svakog jutra mi je govorio da li nešto u podacima od prethodne noći izgleda neobično u odnosu na moj lični baseline. A onda se ispostavilo da meri pogrešnu stvar.
Ovaj deo je o auditu koji je to otkrio, redizajnu koji je iz njega nastao i trenutku kada se Faza 1 zatvorila bez ijednog modela koji bi pobedio svoj naivni baseline po kriterijumu postavljenom unapred.
Empirijska činjenica od koje je počeo redizajn
Pokrenuo sam originalni score protiv buduće fiziologije na celom istorijskom dataset-u. Prozor: 2021-01-01 → 2026-05-15, 1961 dan. Konkretno: upariti današnji readiness sa sutrašnjim izmerenim HRV, RHR i sleep efficiency — i proveriti da li score ima bilo kakvu prognostičku snagu.
Nema je. Današnji readiness praktično ne korelira sa sutrašnjim HRV, RHR ili snom. Same-day unutrašnja konzistentnost je normalna — komponente se slažu međusobno — ali forward predictive power ne postoji. Score radi kao deskriptor trenutnog stanja, ne kao prognoza spremnosti za opterećenje.
To nije bug. Formula je bila metodološki pogrešno nazvana. Redizajn ispravlja upravo taj jaz, a ne težine.
Paralelni nalaz iz istog audita odnosio se na EnergyBank v1 — to je tema 5. dela. Na 387 paired days bio je wrong-signed u odnosu na HRV sledećeg dana (r=−0.149) i RHR sledećeg dana (r=+0.212). Wrong-signed znači da sama ideja nije slomljena, ali je target pogrešno definisan. Verovatno je v1 zapravo hvatao productive strain, a ne recovery deficit. U 5. delu se vraćam na to.
Šta podaci stvarno omogućavaju da se modeluje
Kada sam prestao da pitam „kako da poboljšam score“ i počeo da pitam „šta je iz ovih podataka uopšte predvidivo“, prostor rešenja se ozbiljno suzio.
Daily AR(1)1 za candidate targets je nizak: 0.07–0.28, zavisno od metrike. Na 3-day rolling window iste metrike rastu na 0.50–0.58. To nije kozmetičko zaglađivanje. To je tačka na kojoj predvidivost počinje. Ispod približno 0.5 AR(1), većina varijanse je measurement noise, a model uglavnom pamti sensor jitter.
Presek dana u kojima su istovremeno dostupni HRV, sleep i walking_hr iznosi 889 dana, 45% kalendara. To je osnovni training pool. Audit je takođe izvukao anomalije koje su tražile eksplicitnu obradu:
- Ingest gap u 2024. godini.
walking_heart_rate_averagenedostaje za celu godinu. HRV takođe. RHR je delimično prisutan. Root cause je isti: promena konfiguracije Health Auto Export-a koju tada nisam primetio. Sada je to kodifikovano kao posebansource_epoch, da modeli ne bi tretirali taj prekid kao fiziologiju. - San je 2021. bio samo coarse-only. 322 daily Recovery rows u 2021. i početkom 2022. nisu podobni za sleep efficiency targets, jer su podaci došli iz RingConn / iPhone Sleep Schedule bez stage tracking-a — teritorija 2. i 3. dela. Writer ih ispravno razlikuje od stvarnih out-of-range noći.
minute_metricsne čuvawalking_speed. Minute-level walking segment path je zatvoren; projekat koristi Apple-ov dnevni agregatwalking_heart_rate_average.
Audit je takođe otkrio da Apple Health XML import ne parsira <Workout> elemente. Reprezentativni export.zip — 149 MB kompresovano, 2.4 GB posle raspakivanja — sadržao je 800 strukturiranih treninga za period 2019–2026. U bazi ih je bilo 11. Zapisi iz live workout endpoint-a iOS aplikacije — 102 komada — bili su jedini strukturirani workout podaci koji su ikada učitani. Workout-residual readiness, odnosno kanal „prave sportske spremnosti“, nema production data na kojima može da se računa. Writer za taj sub-score postoji i vraća eligible=false, reason='importer_gap', dok import code ne nauči da parsira <Workout>.
Od readiness-first ka recovery-first
Audit je poništio početni framing. Na ovom dataset-u projekat ne može biti readiness sistem: structured-workout ground truth potreban za readiness ne postoji. Ono što podaci zaista podržavaju merljivim signalom jeste daily recovery + passive efficiency.
Novi oblik je pet sub-scores u tri sloja:
Core daily layer:
1. Recovery Stability → 3d-rolling sleep_efficiency
2. Passive Efficiency → 3d-rolling walking_heart_rate_average
Risk layer (parallel signals, different time horizons):
3. Acute Risk → HRV/RHR/sleep tail events, t+1..t+3
4. Chronic Load → sustained deterioration in Recovery Stability
Optional athletic layer (dormant):
5. Performance Readiness → walking workout HR residual
Sa tim oblikom došle su i ostale dve arhitektonske odluke.
Nema aggregate readiness number. Svaki sub-score odgovara na drugo fiziološko pitanje. Sabiti ih u ponderisani prosek značilo bi ponovo uvesti isti failure mode zbog kog je redizajn i nastao: jedan score bez definisanog target-a. UI umesto toga prikazuje odvojene dimensions i rule-based decision layer.
Targets su 3-day rolling values ili event-window probabilities, a ne single-day points. Daily AR(1) za metrike koje imam ispod je noise floor-a. 3-day rolling window je nivo na kom modeli barem teorijski mogu da poboljšaju smoothed naive baseline.
Faza 0: storage i eligibility
Pre nego što se trenira bilo koji model, writers su morali da nastanu i proizvedu honest labels za celu istoriju. Dodate su tri nove tabele: target_snapshots, feature_snapshots, naive_baselines. Četvrta, source_epochs, čuva katalog ingest/method boundaries na kojima baseline-i treba da se resetuju.
Svaki target zapis sadrži:
target_value— NULL ako je dan ineligible;eligible— bool;eligibility_reason, enum:ok,sleep_data_missing,coarse_only_source,no_walking_hr,baseline_warmup,event_window_data_missing, …;data_coverageJSONB sa per-source coverage koji je uticao na odluku;source_epoch—initial,source_2024_gap,source_2025_current, …
unknown je legitiman rezultat. Dani sa nedostajućim podacima ne popunjavaju se imputacijom. Imputation sistematski krivi evaluaciju baš na danima koji su važni: putovanja, bolest, sat skinut sa ruke. Model ocenjen na imputed labels dobija udoban test set koji ne liči na stvarnost.
Posle punog prolaza kroz istoriju brojevi izgledaju ovako:
| Sub-score | target_kind | eligible (ok) | ineligible (top reason) |
|---|---|---|---|
| Recovery Stability | rolling_3d | 650 | sleep_data_missing 896 |
| Passive Efficiency | rolling_3d | 891 | no_walking_hr 1070 |
| Acute Risk | event_t1_t3 | 1482 (27.5% positive) | baseline_warmup 337 |
| Acute Risk | event_strict_t1_t3 | 1482 (2.3% positive) | baseline_warmup 337 |
| Chronic Load | chronic_label | 469 (17.5% positive) | baseline_warmup 1191 |
| Chronic Load | chronic_acute_density | 386 (76.4% positive) | baseline_warmup 1191 |
Pre bilo kakvog modelovanja ovde su važne dve stvari. Prvo, baseline_warmup dominira u Chronic Load-u, jer mu treba ≥30 eligible rolling-3d Recovery rows u trenutnoj source epoch; prekid iz 2024. spalio je veći deo 2024. i prelio se u Q1-2025. Drugo, chronic_acute_density sa 76.4% positive bio je mis-tuned by construction. Prag ≥3 acute OR-events u 14-dnevnom prozoru niži je od očekivanog broja događaja pri OR base rate od 27.5%: 14 × 0.275 = 3.85. U Fazi 1 threshold je podignut pre nego što je bilo koji model dotakao target.
Faza 1: prvo floors, zatim modeli
Hteo sam da izbegnem standardni ML obrazac: „istreniraj model, pobedi 50% accuracy, proglasi pobedu“. Bez netrivijalnog baseline-a, skoro svaki classifier izgleda dobro na autocorrelated time-series data.
Zato je Faza 1 počela od naive baselines. Oni se računaju i čuvaju kao sopstveni redovi u naive_baselines. Za continuous targets — četiri kandidata: persistence_yesterday, rolling_7d_mean, rolling_30d_mean, ewma_45d. Za classifiers — jedan: event_base_rate, odnosno 90-day rolling prior probability.
Kriterijum uspeha definisan je pre pokretanja bilo kog modela:
- Continuous targets. MAE2 modela na osnovnom chronological 70/30 test-u3 mora da pobedi lower CI bound najboljeg naive baseline-a, uz 1000-iteration block bootstrap4 na 14-dnevnim blokovima — tako se čuva autocorrelation. Samo pobediti point estimate nije dovoljno. To je statistički šum.
- Classifier targets. Lower CI modela za precision@recall=0.55 mora da bude veći od upper CI floor precision@recall=0.5. Intervali ne smeju da se preklapaju. Za sparse labels koristi se stratified bootstrap6: positives i negatives se resampluju odvojeno, da bi se sačuvali class counts.
Jedna suptilnost nije bila odmah vidljiva. persistence_yesterday je pokazao izuzetno dobre rezultate na svim classifier labels: precision 0.785–0.947, AUC 0.829–0.979. To izgleda kao jak floor. Ali nije floor. Labels su forward-window: event_t1_t3 za datum t pokriva t+1..t+3, a za datum t+1 pokriva t+2..t+4. Dva od tri dana su zajednička između susednih labels. Persistence koristi preklapanje label prozora, a ne predviđa unseen physiology. chronic_label gleda 14 dana unapred; susedni labels dele 13 od 14 dana, pa se persistence trivijalno prenosi.
Pošten classifier floor je event_base_rate. Persistence ostaje u izveštaju zbog transparentnosti, ali se ne koristi kao decision metric.
Šta su modeli uradili
Pet pokušaja u Fazi 1. Svi su završili pobedom naive baselines.
Recovery Stability, rolling_3d. Target SD7 0.033. EWMA458 floor MAE 0.0251 (CI lower 0.0231). Najbolji linearni model: Ridge α=1009, MAE 0.0246. Nije pobedio 0.0231. Presuda: no production model; EWMA45 ostaje production layer.
Passive Efficiency, rolling_3d. Target SD 4.226 bpm. EWMA45 floor MAE 3.1911 bpm (CI lower 2.9263). Najbolji linearni model: Ridge α=100, MAE 3.0783 bpm. Nije pobedio 2.9263. Presuda: isto kao Recovery.
Acute Risk, event_t1_t3. 108 test rows, 21 positive (0.194 base rate10). Floor precision@R=0.5 = 0.273 (CI [0.193, 0.394]). L2 logistic α=0.111: precision 0.229 (CI [0.200, 0.750]). CI se preklapaju, lower CI modela je ispod upper CI floor-a. Nema kandidata.
Chronic Load, chronic_label. 97 test rows, 30 positives. Floor precision@R=0.5 = 0.850 (CI [0.682, 1.000]). L2 logistic α=0.01: precision 0.750 (CI [0.593, 1.000]). CI se preklapaju. Presuda: nema modela. Ali linearni model je dao AUC12 0.936 naspram floor 0.794 — jasan ranking-signal gap koji se nije pretvorio u pobedu po precision-at-recall. Taj gap je bio jedino mesto u Fazi 1 gde je non-linearity bila proverljiva hipoteza, a ne ML-hopium, pa sam pokrenuo još jednu probe.
Chronic Load, chronic_label, GBM probe. Zamrznuta 16-cell grid: max_depth × learning_rate × n_estimators × min_samples_leaf, alpha se birala na inner train/val. Najbolja cell na val: md=3, lr=0.1, n=100, leaf=10, val precision@R=0.5 = 0.867. Na held-out test-u: precision 0.714 (CI [0.577, 0.889]), AUC 0.915. Floor nije prošao. Aktiviralo se unapred objavljeno stop rule: ne pokretati GBM13 na chronic_acute_density ili acute_risk. Oni su imali slabiji linear AUC. Faza 1 je zatvorena.
Chronic Load, chronic_acute_density. Posle retuning threshold-a — u Fazi 0 bio je mis-calibrated na 76% positive — 104 test rows, 3 positives. Floor precision 0.042. L2 logistic α=0.01: precision 0.020. Model je značajno gori od floor-a; CI se ne preklapaju na pogrešnoj strani. Model rangira gore od calibrated base rate pri recall = 0.5. Nema kandidata.
Pet pokušaja. Pet presuda: „no production model, naive layer wins“. Na površini to izgleda kao neuspeh. Ali ne u smislu koji je važan.
Recovery Stability, Passive Efficiency, Acute Risk, Chronic Load — četiri production sub-scores. Svaka kartica ima sopstveni status; dashboard ih ne sabija u jedan broj.
Pošteno čitanje rezultata
Faza 1 je dala odbranjivu formu production system-a:
- Continuous targets u production-u opslužuje EWMA45. To nije placeholder. To je izabrani model, proveren kriterijumom strožim od onog po kom je većina consumer-wearable scores ikada ocenjena. To što linearni i tree models nisu uspeli da ga poboljšaju na trenutnom chronological tail-u znači: EWMA45 je prava složenost modela za signal-to-noise ratio ovih podataka.
- Classifier targets opslužuje
event_base_rate. Logika je ista. - Recovery i Passive sub-scores su deskriptivni, ne prediktivni. Govore korisniku gde su se poslednja tri dana našla u odnosu na personal baseline. Ne pokušavaju da predvide sutra.
- Acute Risk u OR-formi radi sa base rate 27.5%, dakle to je noisy alert. Stroga verzija — HRV AND RHR same day — na 2.3% sa 9 positives u 401-row test slice. Operativno je informativna ako se kalibriše; statistički je trenutno previše retka za model.
Postoji realna mogućnost, označena u svakom feasibility doc-u, da je neuspeh modela scope, a ne odsustvo signala. Chronological test tail (2026-01 → 2026-05) ima 21 acute positive, dok su raniji meseci imali 30+. Walk-forward sensitivity za chronic_label pokazuje precision 0.30–1.00 po mesecima, što liči na seasonality ili regime dependence, a ne na beskoristan label. Ali kriterijum ostaje isti: pobediti floor na najsvežijim podacima koje production system zaista ocenjuje, jer upravo taj tail vidi živi sistem. Prirodno je vratiti se na ovo kada se nakupi više positives.
Zašto čuvam verdicts u repozitorijumu
Svaki pokušaj Faze 1 završio je kao Markdown dokument pored koda: READINESS_REDESIGN_PHASE1_PASSIVE_FEASIBILITY.md, ..._RECOVERY_FEASIBILITY.md, ..._CHRONIC_LABEL_FEASIBILITY.md, ..._CHRONIC_LABEL_GBM_PROBE.md, ..._ACUTE_RISK_FEASIBILITY.md, ..._CHRONIC_DENSITY_FEASIBILITY.md. Oni su auto-generated iz analysis scripts, sa zamrznutom methodology i jednim paragraph verdict na dnu.
Važna su dva razloga:
- Re-runnable. Kada se nakupi više positives, ne moram da pamtim koji je bio threshold. Script regeneriše fajl. Verdict se automatski ažurira.
- Poštenje prema sebi. Model koji sam odbacio u maju 2026. za tri meseca će opet delovati zanimljivo. Zamrznuti kriterijum u dokumentu sprečava me da ga tiho omekšam.
Ista logika važi za source-epoch catalogue. HRV gap u 2024. je dokumentovan; spike strict event rate u 2022 — 5.7% naspram 1–2% u drugim godinama, možda illness cluster — označen je za narrative review pre nego što bilo koji model bude treniran na tim features. Ako retrospektivno objasnim 2022. kao „čini mi se da sam bio bolestan u julu“, to objašnjenje će tiho iskriviti buduće modele.
Prava lekcija
Originalni readiness score bio je jedan broj koji spaja tri signala. Redizajn je pet sub-scores koji odbijaju da se spoje. To je promena.
Prethodni članak bio je o poverenju u izvor iza svake kolone. Ovaj je o poverenju u target. Sposobnost da se kaže: „ovo je ono što pokušavamo da predvidimo, ovo je floor koji model mora da pobedi, i evo šta se desilo kada su modeli pokrenuti“. Izlaz izgleda manje impresivno od jednog sigurnog broja. Ali ima manje šanse da bude lepo formatirana laž.
Sledeći članak nastavlja paralelni nalaz iz audita: EnergyBank v1 je bio wrong-signed u odnosu na HRV sledećeg dana. Reč je o tome šta je bilo potrebno da se banka prepravi iz daily snapshot-a u state machine koja prenosi debt između dana i ne laže o propuštenim senzorima.
Jednodnevna autokorelacija unapred. AR(1) je korelacija između današnje vrednosti i jučerašnje vrednosti tokom cele istorije. Vrednosti blizu 0 znače da je signal uglavnom šum iz dana u dan; vrednosti blizu 1 znače da je danas već određen sutra. Za metrike zdravlja, AR(1) iznad približno 0.5 je tačka od koje predviđanje postaje pravi zadatak, a ne samo vežba u pamćenju treperenja senzora. ↩︎
Prosečna apsolutna greška. Prosečna vrednost
|predicted − actual|za sve tačke testa. Standardna regresiona mera: niža je bolja, jedinice odgovaraju jedinicama cilja. Manje kažnjavanje velikih grešaka u poređenju sa RMSE (koji kvadrira greške pre prosečavanja). ↩︎Razvrstavanje podataka po vremenu za treniranje i testiranje. Trenirajte na prvih 70% dana u hronološkom redosledu, testirajte na poslednjih 30%. Ključno za podatke vremenskih serija, jer mešanje dana pre razdvajanja bi dovelo do curenja budućih informacija u skup za trening (model bi učio iz „dana pre” datuma testa, što je nerealno u vreme predviđanja). ↩︎
Metoda ponovnog uzorkovanja za podatke vremenskih serija. Standardni bootstrap meša pojedinačne podatke i prekida svaku vremensku korelaciju između uzastopnih dana, čime uništava strukturu autokorelacije koju skup podataka za testiranje nasleđuje iz stvarnog sveta. Block bootstrap uzorkuje susedne blokove dana (ovde 14-dnevne blokove) tako da se autokorelacija unutar svakog bloka sačuva. ↩︎
Preciznost (deo označenih dana koji su zaista bili pozitivni) merena na pragu koji hvata polovinu pozitivnih (recall = 0.5). Standardna metrika kompromisa za klasifikatore sa retkim događajima: lako je postići visoku preciznost označavanjem samo nekoliko očiglednih slučajeva (nizak recall), i lako je postići visok recall označavanjem svega (niska preciznost). Preciznost@recall=0.5 fiksira poređenje na fiksnoj osetljivosti. ↩︎
Bootstrap koji ponovo uzorkuje pozitivne i negativne klase odvojeno, očuvavajući originalne proporcije klasa u svakom ponovnom uzorku. Bez stratifikacije, bootstrap uzorak iz skupa podataka sa 5% pozitivnih može povremeno imati nula pozitivnih, što čini preciznost nedefinisanom i narušava interval pouzdanosti. ↩︎
Standardna devijacija same ciljne promenljive, u jedinicama skupa za testiranje. Postavlja prirodnu skalu za „koliko je dovoljno dobro”; MAE od 0.025 u odnosu na cilj sa SD od 0.033 znači da je prosečna greška modela uporediva sa jednom standardnom devijacijom osnovnog signala. ↩︎
Eksponencijalno ponderisani pokretni prosek. Osnova za izravnavanje gde noviji dani dobijaju veću težinu od starijih; „45” u EWMA45 označava efektivni poluživot / period poluraspada (~45 dana, nakon kojih je doprinos jednog dana opao na polovinu svoje originalne težine). „Naivna osnova” za predviđanje vremenskih serija koja je zaista teško nadmašiti bez pravog modela. ↩︎
L2-regulisana linearna regresija. Obična linearna regresija, ali sa dodatom kaznom za velike vrednosti koeficijenata:
loss = MSE + α · Σβ².α=100je jačina regularizacije odabrana na validacionom skupu. Veći α znači jači pritisak na manje koeficijente i manje overfitting (preprilagođavanje). Standardna osnova pre nego što se pristupi nelinearnim modelima. ↩︎Udeo pozitivnih primera u skupu podataka. Trivijalni klasifikator „uvek predviđa najčešću klasu” postiže tačnost jednaku osnovnoj stopi, pa bilo koji netrivijalni klasifikator mora jasno da poboljša ovu osnovu. Osnovna stopa od 19.4% znači da je otprilike 1 od 5 dana bilo pozitivno. ↩︎
Logistička regresija (linearni klasifikator koji daje verovatnoće putem logističkog sigmoida) sa istom L2 penom kao i Ridge regresija na koeficijentima. Standardna početna tačka za binarnu klasifikaciju pre stabala i neuronskih mreža. ↩︎
Površina ispod ROC krive (AUC ROC). Metrika rangiranja: verovatnoća da model dodeli višu ocenu nasumičnom pozitivnom nego nasumičnom negativnom. 0.5 je nasumično rangiranje, 1.0 je savršeno. AUC meri da li je redosled modela tačan; precision@recall=0.5 meri da li je prag tačan. Ove dve stvari se mogu razlikovati, što je uradio
chronic_label. ↩︎Mašina za pojačavanje gradijenata. Model ansambla koji redom gradi mnogo plitkih stabala odluka, od kojih svako uči da ispravi greške prethodnih. Standardni „nelinearni osnovni model iznad logističke regresije”; ako GBM ne može da pobedi linearni model, verovatno problem nema nelinearni signal vredan iskorišćavanja na ovoj skali podataka. ↩︎