diff --git a/burden_index.py b/burden_index.py new file mode 100644 index 0000000..398569f --- /dev/null +++ b/burden_index.py @@ -0,0 +1,547 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Literal, Sequence + +import numpy as np +import torch +import torch.nn.functional as F + +from evaluate_auc_v2 import ( + build_model_from_dataset, + load_checkpoint_state_dict, + load_json_config, + load_model_state, + resolve_dist_mode_for_checkpoint, + validate_dataset_metadata, +) +from eval_data import load_sequence_eval_dataset +from targets import CHECKUP_IDX, NO_EVENT_IDX, PAD_IDX + + +FormedBurdenMode = Literal["observed", "model_weighted"] + + +@dataclass(frozen=True) +class BurdenContext: + model: torch.nn.Module + dataset: Any + cfg: dict[str, Any] + dist_mode: str + device: torch.device + run_path: Path + + +@dataclass(frozen=True) +class DiseaseBurdenResult: + disease_id: int + formed: float + future: float + total: float + formed_mode: str + horizon: float + + +@dataclass(frozen=True) +class BurdenIndexResult: + historical: np.ndarray + future: np.ndarray + total: np.ndarray + disease_ids: np.ndarray + formed: np.ndarray + disease_future: np.ndarray + disease_total: np.ndarray + formed_mode: str + horizon: float + + +def load_burden_context( + run_path: str | Path, + *, + device: str | torch.device | None = None, +) -> BurdenContext: + run_path = Path(run_path) + config_path = run_path / "train_config.json" + model_ckpt_path = run_path / "best_model.pt" + if not config_path.exists(): + raise FileNotFoundError(f"train_config.json not found in {run_path}") + if not model_ckpt_path.exists(): + raise FileNotFoundError(f"best_model.pt not found in {run_path}") + + cfg = load_json_config(config_path) + model_target_mode = str(cfg.get("model_target_mode", "next_token")).lower() + if model_target_mode == "next_token": + raise RuntimeError( + "Burden Index computation requires an all_future checkpoint because " + "it uses p_d(h, Delta). The provided run is model_target_mode='next_token'." + ) + if model_target_mode != "all_future": + raise ValueError( + "train_config.json model_target_mode must be all_future for burden " + f"computation, got {model_target_mode!r}." + ) + + device_obj = torch.device( + device if device is not None else ("cuda" if torch.cuda.is_available() else "cpu") + ) + if device_obj.type == "cuda" and not torch.cuda.is_available(): + raise RuntimeError(f"Requested device {device_obj}, but CUDA is not available.") + + data_prefix = cfg.get("data_prefix", "ukb") + labels_file = cfg.get("labels_file", "labels.csv") + extra_info_types = cfg.get("extra_info_types", None) + dataset = load_sequence_eval_dataset( + model_target_mode="all_future", + data_prefix=data_prefix, + labels_file=labels_file, + no_event_interval_years=float(cfg.get("no_event_interval_years", 5.0)), + include_no_event_in_uts_target=bool( + cfg.get("include_no_event_in_uts_target", False) + ), + min_history_events=int(cfg.get("all_future_min_history_events", 1)), + min_future_events=int(cfg.get("all_future_min_future_events", 1)), + extra_info_types=extra_info_types, + ) + validate_dataset_metadata(dataset, cfg) + + state_dict = load_checkpoint_state_dict(model_ckpt_path, map_location="cpu") + dist_mode = resolve_dist_mode_for_checkpoint( + str(cfg.get("dist_mode", "exponential")), + state_dict, + ) + if dist_mode not in {"exponential", "weibull", "mixed"}: + raise ValueError(f"Unsupported dist_mode={dist_mode!r}") + + cfg_model = dict(cfg) + cfg_model["dist_mode"] = dist_mode + args = _ConfigNamespace() + model = build_model_from_dataset(args, cfg_model, dataset) + load_model_state(model, state_dict) + model.eval().to(device_obj) + + return BurdenContext( + model=model, + dataset=dataset, + cfg=cfg, + dist_mode=dist_mode, + device=device_obj, + run_path=run_path, + ) + + +@torch.inference_mode() +def compute_disease_burden( + *, + run_path: str | Path, + disease_id: int, + event_seq: Sequence[int] | np.ndarray, + time_seq: Sequence[float] | np.ndarray, + sex: int, + other_type: Sequence[int] | np.ndarray, + other_value: Sequence[float] | np.ndarray, + other_value_kind: Sequence[int] | np.ndarray, + other_time: Sequence[float] | np.ndarray, + t_query: float, + horizon: float, + formed_mode: FormedBurdenMode, + device: str | torch.device | None = None, + context: BurdenContext | None = None, +) -> DiseaseBurdenResult: + ctx = context or load_burden_context(run_path, device=device) + disease_ids = np.asarray([int(disease_id)], dtype=np.int64) + formed, future_prob = _compute_disease_components( + ctx=ctx, + disease_ids=disease_ids, + event_seq=event_seq, + time_seq=time_seq, + sex=sex, + other_type=other_type, + other_value=other_value, + other_value_kind=other_value_kind, + other_time=other_time, + t_query=float(t_query), + horizon=float(horizon), + formed_mode=formed_mode, + ) + future = (1.0 - formed) * future_prob + total = formed + future + return DiseaseBurdenResult( + disease_id=int(disease_id), + formed=float(formed[0]), + future=float(future[0]), + total=float(total[0]), + formed_mode=str(formed_mode), + horizon=float(horizon), + ) + + +@torch.inference_mode() +def compute_burden_index( + *, + run_path: str | Path, + burden_matrix: np.ndarray, + disease_ids: Sequence[int] | np.ndarray, + event_seq: Sequence[int] | np.ndarray, + time_seq: Sequence[float] | np.ndarray, + sex: int, + other_type: Sequence[int] | np.ndarray, + other_value: Sequence[float] | np.ndarray, + other_value_kind: Sequence[int] | np.ndarray, + other_time: Sequence[float] | np.ndarray, + t_query: float, + horizon: float, + formed_mode: FormedBurdenMode, + device: str | torch.device | None = None, + context: BurdenContext | None = None, +) -> BurdenIndexResult: + ctx = context or load_burden_context(run_path, device=device) + disease_ids_arr = np.asarray(disease_ids, dtype=np.int64) + if disease_ids_arr.ndim != 1 or disease_ids_arr.size == 0: + raise ValueError("disease_ids must be a non-empty 1D sequence.") + + A = np.asarray(burden_matrix, dtype=np.float64) + if A.ndim != 2: + raise ValueError(f"burden_matrix must be 2D, got shape {A.shape}") + if A.shape[1] != disease_ids_arr.size: + raise ValueError( + "burden_matrix columns must match disease_ids length, got " + f"{A.shape[1]} columns vs {disease_ids_arr.size} disease ids." + ) + + formed, future_prob = _compute_disease_components( + ctx=ctx, + disease_ids=disease_ids_arr, + event_seq=event_seq, + time_seq=time_seq, + sex=sex, + other_type=other_type, + other_value=other_value, + other_value_kind=other_value_kind, + other_time=other_time, + t_query=float(t_query), + horizon=float(horizon), + formed_mode=formed_mode, + ) + disease_future = (1.0 - formed) * future_prob + disease_total = formed + disease_future + + historical = A @ formed + future = A @ disease_future + total = A @ disease_total + return BurdenIndexResult( + historical=historical, + future=future, + total=total, + disease_ids=disease_ids_arr.copy(), + formed=formed, + disease_future=disease_future, + disease_total=disease_total, + formed_mode=str(formed_mode), + horizon=float(horizon), + ) + + +class _ConfigNamespace: + def __getattr__(self, _name: str) -> None: + return None + + +def _compute_disease_components( + *, + ctx: BurdenContext, + disease_ids: np.ndarray, + event_seq: Sequence[int] | np.ndarray, + time_seq: Sequence[float] | np.ndarray, + sex: int, + other_type: Sequence[int] | np.ndarray, + other_value: Sequence[float] | np.ndarray, + other_value_kind: Sequence[int] | np.ndarray, + other_time: Sequence[float] | np.ndarray, + t_query: float, + horizon: float, + formed_mode: FormedBurdenMode, +) -> tuple[np.ndarray, np.ndarray]: + formed_mode = _validate_formed_mode(formed_mode) + disease_ids = np.asarray(disease_ids, dtype=np.int64) + _validate_disease_ids(ctx, disease_ids) + event_seq_arr, time_seq_arr = _validate_event_inputs(event_seq, time_seq) + other_type_arr, other_value_arr, other_value_kind_arr, other_time_arr = ( + _validate_other_inputs(other_type, other_value, other_value_kind, other_time) + ) + if horizon < 0: + raise ValueError(f"horizon must be non-negative, got {horizon}") + + if formed_mode == "observed": + formed = _observed_formed_burden( + disease_ids=disease_ids, + event_seq=event_seq_arr, + time_seq=time_seq_arr, + t_query=t_query, + ) + else: + formed = _model_weighted_formed_burden( + ctx=ctx, + disease_ids=disease_ids, + event_seq=event_seq_arr, + time_seq=time_seq_arr, + sex=sex, + other_type=other_type_arr, + other_value=other_value_arr, + other_value_kind=other_value_kind_arr, + other_time=other_time_arr, + t_query=t_query, + ) + + hidden_query = _query_hidden( + ctx=ctx, + event_seq=event_seq_arr, + time_seq=time_seq_arr, + sex=sex, + other_type=other_type_arr, + other_value=other_value_arr, + other_value_kind=other_value_kind_arr, + other_time=other_time_arr, + query_times=np.asarray([t_query], dtype=np.float32), + ) + future_prob = _probabilities_from_hidden( + ctx=ctx, + hidden=hidden_query, + disease_ids=disease_ids, + deltas=np.asarray([horizon], dtype=np.float32), + )[0] + return formed.astype(np.float64), future_prob.astype(np.float64) + + +def _model_weighted_formed_burden( + *, + ctx: BurdenContext, + disease_ids: np.ndarray, + event_seq: np.ndarray, + time_seq: np.ndarray, + sex: int, + other_type: np.ndarray, + other_value: np.ndarray, + other_value_kind: np.ndarray, + other_time: np.ndarray, + t_query: float, +) -> np.ndarray: + grid = _build_readout_grid( + event_seq=event_seq, + time_seq=time_seq, + other_type=other_type, + other_time=other_time, + t_query=t_query, + ) + if grid.size == 0: + return np.zeros(disease_ids.size, dtype=np.float64) + + end_times = np.concatenate([grid[1:], np.asarray([t_query], dtype=np.float32)]) + deltas = np.maximum(end_times - grid, 0.0).astype(np.float32) + if np.all(deltas <= 0): + return np.zeros(disease_ids.size, dtype=np.float64) + + hidden = _query_hidden( + ctx=ctx, + event_seq=event_seq, + time_seq=time_seq, + sex=sex, + other_type=other_type, + other_value=other_value, + other_value_kind=other_value_kind, + other_time=other_time, + query_times=grid.astype(np.float32), + ) + interval_prob = _probabilities_from_hidden( + ctx=ctx, + hidden=hidden, + disease_ids=disease_ids, + deltas=deltas, + ).astype(np.float64) + survival = np.prod(1.0 - np.clip(interval_prob, 0.0, 1.0), axis=0) + return 1.0 - survival + + +def _observed_formed_burden( + *, + disease_ids: np.ndarray, + event_seq: np.ndarray, + time_seq: np.ndarray, + t_query: float, +) -> np.ndarray: + valid = ( + (time_seq <= np.float32(t_query)) + & (event_seq > PAD_IDX) + & (event_seq != CHECKUP_IDX) + & (event_seq != NO_EVENT_IDX) + ) + observed = set(int(x) for x in event_seq[valid].tolist()) + return np.asarray([1.0 if int(d) in observed else 0.0 for d in disease_ids]) + + +def _build_readout_grid( + *, + event_seq: np.ndarray, + time_seq: np.ndarray, + other_type: np.ndarray, + other_time: np.ndarray, + t_query: float, +) -> np.ndarray: + event_mask = (event_seq > PAD_IDX) & (time_seq <= np.float32(t_query)) + other_mask = (other_type > 0) & (other_time <= np.float32(t_query)) + times = np.concatenate( + [ + time_seq[event_mask].astype(np.float32, copy=False), + other_time[other_mask].astype(np.float32, copy=False), + ] + ) + if times.size == 0: + return np.zeros(0, dtype=np.float32) + return np.unique(times) + + +def _query_hidden( + *, + ctx: BurdenContext, + event_seq: np.ndarray, + time_seq: np.ndarray, + sex: int, + other_type: np.ndarray, + other_value: np.ndarray, + other_value_kind: np.ndarray, + other_time: np.ndarray, + query_times: np.ndarray, +) -> torch.Tensor: + if query_times.ndim != 1: + raise ValueError("query_times must be 1D.") + batch_size = int(query_times.size) + if batch_size == 0: + return torch.empty(0, ctx.model.n_embd, device=ctx.device) + + event = torch.from_numpy(event_seq[None, :].repeat(batch_size, axis=0)).long() + times = torch.from_numpy(time_seq[None, :].repeat(batch_size, axis=0)).float() + other_t = torch.from_numpy(other_type[None, :].repeat(batch_size, axis=0)).long() + other_v = torch.from_numpy(other_value[None, :].repeat(batch_size, axis=0)).float() + other_k = torch.from_numpy( + other_value_kind[None, :].repeat(batch_size, axis=0) + ).long() + other_tm = torch.from_numpy(other_time[None, :].repeat(batch_size, axis=0)).float() + sex_t = torch.full((batch_size,), int(sex), dtype=torch.long) + tq = torch.from_numpy(query_times.astype(np.float32, copy=False)).float() + + event = event.to(ctx.device) + times = times.to(ctx.device) + other_t = other_t.to(ctx.device) + other_v = other_v.to(ctx.device) + other_k = other_k.to(ctx.device) + other_tm = other_tm.to(ctx.device) + sex_t = sex_t.to(ctx.device) + tq = tq.to(ctx.device) + + return ctx.model( + event_seq=event, + time_seq=times, + sex=sex_t, + padding_mask=event > PAD_IDX, + t_query=tq, + other_type=other_t, + other_value=other_v, + other_value_kind=other_k, + other_time=other_tm, + target_mode="all_future", + ) + + +def _probabilities_from_hidden( + *, + ctx: BurdenContext, + hidden: torch.Tensor, + disease_ids: np.ndarray, + deltas: np.ndarray, +) -> np.ndarray: + if hidden.ndim != 2: + raise ValueError(f"hidden must have shape (N, H), got {tuple(hidden.shape)}") + if deltas.ndim != 1 or deltas.size != hidden.shape[0]: + raise ValueError( + "deltas must be 1D with the same length as hidden rows, got " + f"{deltas.shape} vs {tuple(hidden.shape)}" + ) + ids = torch.as_tensor(disease_ids, dtype=torch.long, device=ctx.device) + logits = ctx.model.calc_risk(hidden)[:, ids] + rate = F.softplus(logits).clamp_min(1e-8) + delta_t = torch.as_tensor(deltas, dtype=rate.dtype, device=ctx.device).clamp_min(0) + + if ctx.dist_mode == "weibull": + rho = ctx.model.calc_weibull_rho(hidden)[:, ids] + exposure = torch.pow(delta_t[:, None], rho) + elif ctx.dist_mode == "mixed": + exposure = delta_t[:, None].expand_as(rate) + death_idx = int(getattr(ctx.model, "death_idx", getattr(ctx.model, "vocab_size", 0) - 1)) + death_cols = [j for j, token in enumerate(disease_ids.tolist()) if int(token) == death_idx] + if death_cols: + death_rho = ctx.model.calc_death_rho(hidden) + for col in death_cols: + exposure[:, int(col)] = torch.pow(delta_t, death_rho) + else: + exposure = delta_t[:, None].expand_as(rate) + + prob = -torch.expm1(-rate * exposure) + return prob.detach().cpu().numpy().astype(np.float64, copy=False) + + +def _validate_formed_mode(mode: str) -> FormedBurdenMode: + if mode not in {"observed", "model_weighted"}: + raise ValueError( + "formed_mode must be either 'observed' or 'model_weighted', " + f"got {mode!r}." + ) + return mode # type: ignore[return-value] + + +def _validate_disease_ids(ctx: BurdenContext, disease_ids: np.ndarray) -> None: + if disease_ids.ndim != 1 or disease_ids.size == 0: + raise ValueError("disease_ids must be a non-empty 1D array.") + vocab_size = int(getattr(ctx.model, "vocab_size", ctx.model.risk_head.out_features)) + if np.any(disease_ids < 0) or np.any(disease_ids >= vocab_size): + raise ValueError(f"disease_ids must be in [0, {vocab_size}), got {disease_ids}") + + +def _validate_event_inputs( + event_seq: Sequence[int] | np.ndarray, + time_seq: Sequence[float] | np.ndarray, +) -> tuple[np.ndarray, np.ndarray]: + events = np.asarray(event_seq, dtype=np.int64) + times = np.asarray(time_seq, dtype=np.float32) + if events.ndim != 1 or times.ndim != 1: + raise ValueError("event_seq and time_seq must be 1D.") + if events.shape != times.shape: + raise ValueError( + f"event_seq and time_seq must have the same shape, got {events.shape} vs {times.shape}" + ) + if events.size == 0: + raise ValueError("event_seq must contain at least one token.") + if not np.all(np.isfinite(times)): + raise ValueError("time_seq contains non-finite values.") + return events, times + + +def _validate_other_inputs( + other_type: Sequence[int] | np.ndarray, + other_value: Sequence[float] | np.ndarray, + other_value_kind: Sequence[int] | np.ndarray, + other_time: Sequence[float] | np.ndarray, +) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: + typ = np.asarray(other_type, dtype=np.int64) + val = np.asarray(other_value, dtype=np.float32) + kind = np.asarray(other_value_kind, dtype=np.int64) + tm = np.asarray(other_time, dtype=np.float32) + if not (typ.shape == val.shape == kind.shape == tm.shape): + raise ValueError( + "other_type, other_value, other_value_kind, and other_time must " + f"have the same shape, got {typ.shape}, {val.shape}, {kind.shape}, {tm.shape}." + ) + if typ.ndim != 1: + raise ValueError("other_* inputs must be 1D.") + if not np.all(np.isfinite(tm)): + raise ValueError("other_time contains non-finite values.") + return typ, val, kind, tm diff --git a/burden_index_method.tex b/burden_index_method.tex new file mode 100644 index 0000000..4565b42 --- /dev/null +++ b/burden_index_method.tex @@ -0,0 +1,576 @@ +\documentclass[11pt]{article} + +\usepackage[margin=1in]{geometry} +\usepackage{amsmath, amssymb, amsfonts} +\usepackage{bm} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{hyperref} + +\title{DeepHealth Burden Indices: Dynamic Organ and Functional Burden} +\author{} +\date{} + +\begin{document} + +\maketitle + +\begin{abstract} +DeepHealth produces a query-time hidden representation \(h(t)\) and +disease-specific future risk functions \(p_d(h,\Delta)\). These disease-level +outputs are clinically granular but difficult to interpret directly as a +patient-level health state. We therefore define Burden Indices (BI) that +aggregate historical and predicted disease burden into higher-level, +interpretable dimensions. The Organ Burden Index (OBI) maps diseases to +anatomical systems, while the Functional Burden Index (FBI) maps diseases to +function- and frailty-related burden domains, anchored by CIHI-HFRM-style +diagnosis weights when available. For formed burden, we distinguish an +observed-anchored version based on actual historical diagnoses and a +model-weighted version based on DeepHealth's historical risk trajectory. The +indices are burden measures, not direct health reserve measures, because the +current model is supervised by disease events rather than direct functional +outcomes such as ADL/IADL, gait speed, grip strength, cognition, or recovery +capacity. +\end{abstract} + +\section{Motivation} + +At query time \(t\), DeepHealth produces a hidden state \(h(t)\) and +disease-level risk predictions +\[ + p_d(h(t), \tau), \qquad d = 1,\ldots,D, +\] +where \(p_d(h(t),\tau)\) is the predicted probability of disease \(d\) occurring +within future horizon \(\tau\). These outputs are useful for disease-specific +risk prediction, but they do not directly answer patient-level questions such as +where disease burden is concentrated or how much functional vulnerability is +implied by the disease profile. + +We introduce Burden Indices to summarize disease-level predictions into +interpretable state representations: +\[ + \text{disease-level risk} + \quad \longrightarrow \quad + \text{system-level burden}. +\] +The indices combine two components: +\begin{enumerate}[leftmargin=*] + \item formed burden: disease burden already accumulated by query time \(t\); + \item future expected burden: disease burden expected to newly form within + horizon \(\tau\). +\end{enumerate} + +At the current stage, these quantities should be called burden indices rather +than health reserve or health state scores. The current model is trained and +validated primarily on ICD disease events. Its directly verifiable semantics are +disease occurrence and disease risk. Without direct functional labels or a +calibrated healthy reference state, quantities such as \(100-\text{burden}\) +cannot be rigorously interpreted as remaining health reserve. + +\section{Two Burden Spaces} + +We define two complementary burden spaces. + +\subsection{Organ Burden Index} + +The Organ Burden Index (OBI) maps disease burden to anatomical systems. It +answers: +\[ + \text{Which organs or anatomical systems carry the largest pathological burden?} +\] +Typical dimensions may include heart/vascular, brain/neurological, kidney, +lung, liver/digestive, metabolic/endocrine, musculoskeletal, hematologic, and +malignancy-related systems. The mapping matrix is denoted +\[ + A^{\mathrm{organ}} \in \mathbb{R}_{\ge 0}^{K_o \times D}, +\] +where \(A^{\mathrm{organ}}_{k,d}\) is the contribution weight from disease +\(d\) to organ dimension \(k\). + +\subsection{Functional Burden Index} + +The Functional Burden Index (FBI) maps disease burden to function- and +frailty-related diagnostic burden domains. It answers: +\[ + \text{How much functional vulnerability is implied by the disease burden?} +\] +Candidate dimensions include mobility burden, cognition burden, mood burden, +sensory burden, nutrition burden, infection or immune vulnerability burden, +functional dependence burden, and comorbidity burden. + +When CIHI-HFRM or another validated hospital frailty risk measure code list is +available, it should be used as the primary anchor for FBI. The mapping matrix +is denoted +\[ + A^{\mathrm{func}} \in \mathbb{R}_{\ge 0}^{K_f \times D}, +\] +where \(A^{\mathrm{func}}_{k,d}\) is the contribution weight from disease \(d\) +to functional burden dimension \(k\). + +OBI and FBI are not redundant. OBI describes where pathology is concentrated, +whereas FBI describes how disease burden may translate into functional +vulnerability. For example, stroke contributes primarily to brain/vascular +burden in OBI, but may contribute to mobility, cognition, sensory, and +functional dependence burden in FBI. + +\section{Model Outputs and Disease Risk Function} + +For each hidden state \(h\), DeepHealth defines a disease-specific future risk +function +\[ + p_d(h,\Delta), +\] +where \(\Delta \ge 0\) is the time horizon. The risk function is produced by the +all-future model. Let +\[ + \eta_d(h) = \operatorname{risk\_head}(h)_d, + \qquad + \lambda_d(h) = \operatorname{softplus}(\eta_d(h)). +\] + +For the exponential all-future model, +\[ + p_d(h,\Delta) + = + 1-\exp[-\lambda_d(h)\Delta]. +\] + +For the Weibull all-future model, with +\[ + \rho_d(h)=\operatorname{softplus}(\operatorname{rho\_head}(h)_d), +\] +the risk function is +\[ + p_d(h,\Delta) + = + 1-\exp[-\lambda_d(h)\Delta^{\rho_d(h)}]. +\] + +The burden formulation below only assumes access to \(p_d(h,\Delta)\); the +exponential and Weibull cases are specializations. + +\section{Formed Burden} + +For a patient queried at time \(t\), let the available historical readout times +be +\[ + t_0 < t_1 < \cdots < t_n \le t. +\] +For notational convenience, define +\[ + t_{n+1}=t. +\] +The historical trajectory is partitioned into adjacent, non-overlapping +intervals +\[ + [t_i,t_{i+1}], \qquad i=0,\ldots,n. +\] +Let +\[ + h_i = h(t_i), \qquad \Delta_i = t_{i+1}-t_i. +\] + +The interval-level model-implied probability for disease \(d\) is +\[ + q_{d,i}(t) = p_d(h_i,\Delta_i). +\] + +\subsection{Model-Weighted Formed Burden} + +The model-weighted formed burden uses DeepHealth's own historical risk +trajectory to quantify how strongly disease \(d\) is represented as formed +burden by time \(t\). It is defined by noisy-or accumulation over historical +intervals: +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_{i=0}^{n}\left[1-q_{d,i}(t)\right]. +\] +Equivalently, +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_{i=0}^{n} + \left[ + 1-p_d\!\left(h(t_i),t_{i+1}-t_i\right) + \right], + \qquad t_{n+1}=t. +\] + +This definition uses each segment of the historical trajectory exactly once and +therefore avoids repeatedly counting overlapping predictions from multiple +historical states to the same query time. + +\subsection{Observed-Anchored Formed Burden} + +The observed-anchored formed burden treats historical diagnoses as factual +evidence. Define the observed historical disease indicator +\[ + o_d(t) + = + \mathbb{I}\left\{ + \exists j:\; \mathrm{event}_j=d,\; \mathrm{time}_j\le t + \right\}. +\] +The observed-anchored version is +\[ + z^{\mathrm{obs}}_d(t)=o_d(t). +\] + +This version is closest to diagnosis-code burden measures such as HFRM: once a +disease code has appeared before query time \(t\), the corresponding disease +burden component is considered present. It is maximally auditable and aligned +with code-based burden definitions, but it does not distinguish severity, +recency, or residual impact among patients with the same historical diagnosis. + +\subsection{Choice of Formed Burden} + +The two definitions represent different semantics: +\[ + z^{\mathrm{obs}}_d(t) + = + \text{observed diagnostic burden}, +\] +\[ + z^{\mathrm{model}}_d(t) + = + \text{model-weighted state burden}. +\] +The observed-anchored version should be used when the goal is to reproduce or +extend diagnosis-code burden measures. The model-weighted version should be used +when the goal is to let DeepHealth assign a continuous burden strength based on +the historical hidden-state trajectory. In the formulas below, \(z_d(t)\) +denotes either \(z^{\mathrm{obs}}_d(t)\) or \(z^{\mathrm{model}}_d(t)\), depending +on the selected BI variant. + +\subsection{Observed-Anchored versus Model-Weighted Burden} + +The observed-anchored and model-weighted definitions share the same purpose: +both quantify disease burden already formed by query time \(t\), before adding +future expected burden. They also use the same downstream BI equations; the only +difference is the definition of \(z_d(t)\). + +Their key difference is the evidence treated as primary. The observed-anchored +version treats diagnosis occurrence as the primary unit of evidence: +\[ + z^{\mathrm{obs}}_d(t)=1 + \quad\text{once disease } d \text{ has been observed before } t. +\] +This is appropriate when the burden index is intended to remain close to +diagnosis-code measures such as HFRM. It is transparent and robust to model +miscalibration, but it treats all historical occurrences of the same disease as +equally formed burden. + +The model-weighted version treats the DeepHealth risk trajectory as the primary +unit of evidence: +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_i + \left[ + 1-p_d\!\left(h(t_i),t_{i+1}-t_i\right) + \right]. +\] +This version can assign different burden strengths to the same observed disease +depending on timing, surrounding history, extra-info context, and the hidden +state trajectory. It may better reflect state-dependent burden intensity, but it +can also downweight a disease that was truly observed if the model assigns low +historical probability. + +Thus the two variants answer related but non-identical questions: +\[ + z^{\mathrm{obs}}_d(t): + \text{Has disease } d \text{ been recorded as part of the patient's history?} +\] +\[ + z^{\mathrm{model}}_d(t): + \text{How strongly does the model-implied trajectory support burden from } + d \text{ by time } t? +\] +For this reason, both should be considered useful sensitivity variants. The +observed-anchored version is preferable for auditability and alignment with +existing code-based indices. The model-weighted version is preferable when the +goal is to use DeepHealth as a continuous state model and allow the learned +trajectory to modulate burden strength. + +\subsection{Cumulative Intensity Form} + +Define the interval cumulative intensity +\[ + \ell_d(h_i,\Delta_i) + = + -\log\left[1-p_d(h_i,\Delta_i)\right], +\] +and the accumulated historical intensity +\[ + \Lambda^{\mathrm{model}}_d(t)=\sum_{i=0}^{n}\ell_d(h_i,\Delta_i). +\] +Then +\[ + z^{\mathrm{model}}_d(t)=1-\exp[-\Lambda^{\mathrm{model}}_d(t)]. +\] + +For the exponential model, +\[ + \ell_d(h_i,\Delta_i)=\lambda_d(h_i)\Delta_i, +\] +so +\[ + z^{\mathrm{model}}_d(t) + = + 1-\exp\left[ + -\sum_{i=0}^{n} + \lambda_d\!\left(h(t_i)\right)(t_{i+1}-t_i) + \right]. +\] + +For the Weibull model, +\[ + \ell_d(h_i,\Delta_i) + = + \lambda_d(h_i)\Delta_i^{\rho_d(h_i)}, +\] +so +\[ + z^{\mathrm{model}}_d(t) + = + 1-\exp\left[ + -\sum_{i=0}^{n} + \lambda_d\!\left(h(t_i)\right) + (t_{i+1}-t_i)^{\rho_d(h(t_i))} + \right]. +\] + +\section{Future Expected Burden} + +The selected formed burden \(z_d(t)\) represents disease burden already formed +by query time \(t\). It can be either the observed-anchored burden +\(z^{\mathrm{obs}}_d(t)\) or the model-weighted burden +\(z^{\mathrm{model}}_d(t)\). The current future risk from query time \(t\) to +horizon \(\tau\) is +\[ + p_d(h(t),\tau). +\] +The future expected newly formed burden for disease \(d\) is defined as +\[ + f_d(t,\tau) + = + \left[1-z_d(t)\right]p_d(h(t),\tau). +\] +This term counts only the portion of disease burden that has not already formed +by time \(t\). The total dynamic disease burden contribution is +\[ + b_d(t,\tau) + = + z_d(t)+f_d(t,\tau). +\] +Equivalently, +\[ + b_d(t,\tau) + = + 1- + \left[1-z_d(t)\right] + \left[1-p_d(h(t),\tau)\right]. +\] +Thus \(b_d(t,\tau)\) can be interpreted as the probability that disease burden +for \(d\) has formed by time \(t\) or will newly form within the future horizon +\(\tau\). + +\section{Burden Index Definition} + +Let \(A \in \mathbb{R}_{\ge 0}^{K \times D}\) be a disease-to-burden mapping +matrix. The historical, future, and total burden indices for dimension \(k\) +are +\[ + \operatorname{BI}^{\mathrm{hist}}_k(t) + = + \sum_{d=1}^{D} A_{k,d} z_d(t), +\] +\[ + \operatorname{BI}^{\mathrm{future}}_k(t,\tau) + = + \sum_{d=1}^{D} + A_{k,d} + \left[1-z_d(t)\right]p_d(h(t),\tau), +\] +and +\[ + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + = + \operatorname{BI}^{\mathrm{hist}}_k(t) + + + \operatorname{BI}^{\mathrm{future}}_k(t,\tau). +\] +Equivalently, +\[ + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + = + \sum_{d=1}^{D} + A_{k,d} + \left\{ + 1- + \left[1-z_d(t)\right] + \left[1-p_d(h(t),\tau)\right] + \right\}. +\] + +For OBI, \(A=A^{\mathrm{organ}}\). For FBI, \(A=A^{\mathrm{func}}\). + +\section{Constructing the Mapping Matrices} + +\subsection{Organ Mapping Matrix} + +The organ mapping matrix should be constructed from code taxonomy or validated +clinical grouping systems rather than manually assigned arbitrary weights. In +the current ICD-token setting, the first version can use ICD chapters or +predefined ICD ranges to construct a sparse disease-to-organ mask +\[ + M^{\mathrm{organ}}_{k,d}\in\{0,1\}. +\] +Examples include: +\begin{center} +\begin{tabular}{ll} +\toprule +Dimension & Example ICD groups \\ +\midrule +Heart/vascular & I00--I99, optionally split into cardiac and vascular groups \\ +Brain/neurological & G00--G99, F00--F09, I60--I69 \\ +Kidney/urogenital & N00--N39, especially N17--N19 \\ +Lung/respiratory & J00--J99 \\ +Metabolic/endocrine & E00--E90 \\ +Liver/digestive & K00--K93, especially K70--K77 \\ +Musculoskeletal & M00--M99 \\ +\bottomrule +\end{tabular} +\end{center} + +The simplest organ weights are +\[ + A^{\mathrm{organ}}_{k,d}=M^{\mathrm{organ}}_{k,d}. +\] +If longitudinal organ endpoint labels are available, the weights can be learned +under the mask: +\[ + A^{\mathrm{organ}}_{k,d}\ge 0, + \qquad + A^{\mathrm{organ}}_{k,d}=0 + \quad\text{if}\quad + M^{\mathrm{organ}}_{k,d}=0. +\] +This keeps the projection clinically interpretable while allowing data-driven +calibration. + +\subsection{Functional Mapping Matrix} + +The functional mapping matrix should be anchored by a validated frailty-related +diagnosis code set whenever possible. CIHI-HFRM or a closely related Hospital +Frailty Risk Measure provides a suitable starting point because it defines +frailty burden from diagnosis codes and associated weights. + +Let \(w^{\mathrm{HFRM}}_d\ge 0\) be the HFRM weight mapped to DeepHealth disease +token \(d\). If the HFRM code list is more granular than the DeepHealth ICD +token vocabulary, weights should be mapped by code prefix. For three-character +ICD tokens, a conservative default is +\[ + w^{\mathrm{HFRM}}_d + = + \max_{c:\, c \text{ maps to token } d} + w^{\mathrm{HFRM}}_c. +\] + +For total functional burden, the one-dimensional mapping is +\[ + A^{\mathrm{func,total}}_{1,d} + = + w^{\mathrm{HFRM}}_d. +\] +For domain-specific functional burden, define a grouping mask +\[ + G_{k,d}\in\{0,1\}, +\] +where \(G_{k,d}=1\) means HFRM-associated disease token \(d\) belongs to +functional burden domain \(k\). Then +\[ + A^{\mathrm{func}}_{k,d} + = + G_{k,d} w^{\mathrm{HFRM}}_d. +\] + +Candidate functional domains include mobility, cognition, mood, sensory, +nutrition, infection or immune vulnerability, functional dependence, and +comorbidity burden. These domain labels should be treated as diagnostic-burden +proxies unless direct functional measurements are available for calibration. + +\section{Normalization and Reporting} + +The raw burden index is an additive weighted burden: +\[ + \operatorname{BI}_k(t,\tau) + = + \sum_d A_{k,d} b_d(t,\tau). +\] +For interpretability, the system should report the decomposition +\[ + \operatorname{BI}^{\mathrm{hist}}_k(t), + \qquad + \operatorname{BI}^{\mathrm{future}}_k(t,\tau), + \qquad + \operatorname{BI}^{\mathrm{total}}_k(t,\tau). +\] + +Optionally, a normalized burden can be reported as +\[ + \widetilde{\operatorname{BI}}_k(t,\tau) + = + \frac{ + \sum_d A_{k,d} b_d(t,\tau) + }{ + \sum_d A_{k,d} + \epsilon + }, +\] +where \(\epsilon>0\) prevents division by zero. This normalized score lies on a +dimension-comparable scale when \(b_d(t,\tau)\in[0,1]\) and \(A_{k,d}\ge 0\). + +For cohort-level interpretation, an additional percentile score can be computed +within age- and sex-specific reference strata: +\[ + \operatorname{PercentileBI}_k(t,\tau) + = + \operatorname{rank}_{\mathrm{age,sex}} + \left( + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + \right). +\] +This percentile is a relative burden ranking, not a health reserve percentage. + +\section{Validation} + +OBI and FBI should be validated against different endpoints. + +For OBI, validation endpoints should be organ-system-specific future events, for +example cardiac events for heart/vascular burden, stroke or dementia for +brain/neurological burden, CKD progression for kidney burden, and respiratory +events for lung burden. + +For FBI, validation should use CIHI-HFRM-style frailty burden, frailty-related +diagnosis endpoints, hospitalization, mortality, care dependence proxies, or +direct functional outcomes if available. If direct functional labels such as +ADL/IADL, gait speed, grip strength, cognitive tests, or recovery measures are +not available, FBI should be reported as a diagnosis-risk-based functional +burden proxy rather than a direct functional reserve measure. + +\section{Summary} + +DeepHealth Burden Indices transform disease-level risk predictions into +interpretable burden representations. Formed burden can be defined either as +observed-anchored burden \(z^{\mathrm{obs}}_d(t)\), which follows factual +diagnosis history, or as model-weighted burden \(z^{\mathrm{model}}_d(t)\), +which accumulates DeepHealth's predicted interval risks along the hidden-state +trajectory. The future expected burden is the residual future risk among disease +burden not already formed. OBI uses anatomical disease groupings to summarize +where pathological burden is concentrated. FBI uses CIHI-HFRM-style +frailty-related diagnosis weights to summarize functional vulnerability burden. +Together, they provide two complementary views of disease burden while allowing +the formed-burden semantics to be chosen explicitly. + +\end{document} diff --git a/burden_index_method_zh.tex b/burden_index_method_zh.tex new file mode 100644 index 0000000..8af59f9 --- /dev/null +++ b/burden_index_method_zh.tex @@ -0,0 +1,491 @@ +\documentclass[11pt]{ctexart} + +\usepackage[margin=1in]{geometry} +\usepackage{amsmath, amssymb, amsfonts} +\usepackage{bm} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{hyperref} + +\title{DeepHealth 负担指数:动态器官负担与功能负担} +\author{} +\date{} + +\begin{document} + +\maketitle + +\begin{abstract} +DeepHealth 在查询时刻 \(t\) 输出隐含状态 \(h(t)\),并基于该隐含状态给出疾病级未来风险函数 +\(p_d(h,\Delta)\)。疾病级输出足够细,但很难直接解释为个体层面的健康状态。因此,我们定义负担指数 +(Burden Indices, BI),将历史已经形成的疾病负担和未来预期新增疾病负担聚合为更高层、可解释的表征。 +器官负担指数(Organ Burden Index, OBI)将疾病映射到解剖系统;功能负担指数 +(Functional Burden Index, FBI)将疾病映射到功能受损和衰弱相关的诊断负担维度,并在条件允许时使用 +CIHI-HFRM 风格的诊断代码权重作为锚点。对于已形成负担,我们区分基于真实历史诊断的观测锚定版本, +以及基于 DeepHealth 历史风险轨迹的模型加权版本。当前阶段这些指标应被称为负担指数,而不是健康储备或健康状态评分, +因为当前模型主要由疾病事件监督,而不是由 ADL/IADL、步速、握力、认知测验或恢复能力等真实功能结局监督。 +\end{abstract} + +\section{动机} + +在查询时刻 \(t\),DeepHealth 输出隐含状态 \(h(t)\),并给出疾病级风险预测 +\[ + p_d(h(t), \tau), \qquad d = 1,\ldots,D, +\] +其中 \(p_d(h(t),\tau)\) 表示疾病 \(d\) 在未来时间窗 \(\tau\) 内发生的预测概率。疾病级风险适合做单病种预测, +但不能直接回答个体层面的状态问题,例如疾病负担集中在哪些系统、当前已经形成了多少疾病负担、未来还可能新增多少负担, +以及疾病风险可能带来多少功能脆弱性。 + +因此,我们引入负担指数,将疾病级预测压缩为系统级负担表征: +\[ + \text{疾病级风险} + \quad \longrightarrow \quad + \text{系统级负担}. +\] +负担指数由两部分组成: +\begin{enumerate}[leftmargin=*] + \item 已形成负担:截至查询时刻 \(t\) 已经累积形成的疾病负担; + \item 未来预期负担:未来时间窗 \(\tau\) 内预期新增形成的疾病负担。 +\end{enumerate} + +当前阶段,这些量应称为负担指数,而不是健康储备或完整健康状态。原因是当前模型主要基于 ICD 疾病事件训练和验证, +其直接可验证语义是疾病发生和疾病风险,而不是真实功能表现或生理储备。没有直接功能标签或健康参考系校准时, +\(100-\text{负担}\) 不能被严谨解释为剩余健康储备。 + +\section{两类负担空间} + +我们定义两类互补的负担空间。 + +\subsection{器官负担指数} + +器官负担指数(OBI)将疾病负担映射到解剖系统,回答: +\[ + \text{病理负担主要集中在哪些器官或系统?} +\] +典型维度可以包括心脏/血管、脑/神经、肾脏、肺、肝脏/消化、代谢/内分泌、肌骨、血液以及肿瘤相关系统。 +其映射矩阵记为 +\[ + A^{\mathrm{organ}} \in \mathbb{R}_{\ge 0}^{K_o \times D}, +\] +其中 \(A^{\mathrm{organ}}_{k,d}\) 表示疾病 \(d\) 对器官维度 \(k\) 的贡献权重。 + +\subsection{功能负担指数} + +功能负担指数(FBI)将疾病负担映射到功能受损和衰弱相关的诊断负担维度,回答: +\[ + \text{这些疾病负担提示了多少功能脆弱性?} +\] +候选维度包括行动负担、认知负担、情绪负担、感官负担、营养负担、感染或免疫脆弱性负担、功能依赖负担和共病负担。 + +如果可以获得 CIHI-HFRM 或其他经过验证的住院衰弱风险诊断代码表,应优先将其作为 FBI 的锚点。 +其映射矩阵记为 +\[ + A^{\mathrm{func}} \in \mathbb{R}_{\ge 0}^{K_f \times D}, +\] +其中 \(A^{\mathrm{func}}_{k,d}\) 表示疾病 \(d\) 对功能负担维度 \(k\) 的贡献权重。 + +OBI 与 FBI 不是重复指标。OBI 描述病理负担的位置,FBI 描述疾病负担可能带来的功能脆弱性。例如,卒中在 OBI 中主要贡献脑/血管负担, +但在 FBI 中可能同时贡献行动、认知、感官和功能依赖负担。 + +\section{模型输出与疾病风险函数} + +对任意隐含状态 \(h\),DeepHealth 定义疾病级未来风险函数 +\[ + p_d(h,\Delta), +\] +其中 \(\Delta \ge 0\) 是时间窗长度。该风险函数由 all-future 模型给出。记 +\[ + \eta_d(h) = \operatorname{risk\_head}(h)_d, + \qquad + \lambda_d(h) = \operatorname{softplus}(\eta_d(h)). +\] + +对于 exponential all-future 模型, +\[ + p_d(h,\Delta) + = + 1-\exp[-\lambda_d(h)\Delta]. +\] + +对于 Weibull all-future 模型,另有 +\[ + \rho_d(h)=\operatorname{softplus}(\operatorname{rho\_head}(h)_d), +\] +其风险函数为 +\[ + p_d(h,\Delta) + = + 1-\exp[-\lambda_d(h)\Delta^{\rho_d(h)}]. +\] + +下面的负担定义只要求能够访问 \(p_d(h,\Delta)\)。exponential 和 Weibull 是该定义的两个具体实现。 + +\section{已形成负担} + +对一个在时刻 \(t\) 查询的个体,设其历史 readout 时间点为 +\[ + t_0 < t_1 < \cdots < t_n \le t. +\] +为方便记号,定义 +\[ + t_{n+1}=t. +\] +于是历史轨迹被划分为相邻且不重叠的区间 +\[ + [t_i,t_{i+1}], \qquad i=0,\ldots,n. +\] +记 +\[ + h_i = h(t_i), \qquad \Delta_i = t_{i+1}-t_i. +\] + +疾病 \(d\) 在第 \(i\) 个历史区间内的模型推断概率为 +\[ + q_{d,i}(t) = p_d(h_i,\Delta_i). +\] + +\subsection{模型加权已形成负担} + +模型加权已形成负担使用 DeepHealth 自身的历史风险轨迹,刻画疾病 \(d\) 在查询时刻 \(t\) 之前已经形成的负担强度。 +它通过 noisy-or 沿历史区间累积定义: +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_{i=0}^{n}\left[1-q_{d,i}(t)\right]. +\] +等价地, +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_{i=0}^{n} + \left[ + 1-p_d\!\left(h(t_i),t_{i+1}-t_i\right) + \right], + \qquad t_{n+1}=t. +\] + +该定义只使用每一段历史区间一次,因此避免了从多个历史节点重复预测到同一个查询时刻所造成的重叠计数。 + +\subsection{观测锚定已形成负担} + +观测锚定已形成负担将历史诊断视为事实证据。定义疾病 \(d\) 的历史观测指示变量: +\[ + o_d(t) + = + \mathbb{I}\left\{ + \exists j:\; \mathrm{event}_j=d,\; \mathrm{time}_j\le t + \right\}. +\] +观测锚定版本定义为 +\[ + z^{\mathrm{obs}}_d(t)=o_d(t). +\] + +该版本最接近 HFRM 等基于诊断代码的负担度量:一旦疾病代码在查询时刻 \(t\) 前出现,对应的疾病负担项即被视为存在。 +它最可审计,也最贴近代码负担定义,但不能区分同一历史诊断在严重程度、发生时间远近和当前残留影响上的差异。 + +\subsection{已形成负担版本选择} + +两个定义对应不同语义: +\[ + z^{\mathrm{obs}}_d(t) + = + \text{观测诊断负担}, +\] +\[ + z^{\mathrm{model}}_d(t) + = + \text{模型加权状态负担}. +\] +当目标是复现或扩展诊断代码负担指标时,应使用观测锚定版本。当目标是让 DeepHealth 根据历史隐含状态轨迹给出连续负担强度时, +应使用模型加权版本。在后续公式中,\(z_d(t)\) 表示根据所选 BI 版本采用的 +\(z^{\mathrm{obs}}_d(t)\) 或 \(z^{\mathrm{model}}_d(t)\)。 + +\subsection{观测锚定与模型加权负担的异同} + +观测锚定和模型加权两个定义具有相同目标:二者都试图刻画查询时刻 \(t\) 之前已经形成的疾病负担,并在此基础上再叠加未来预期负担。 +二者进入后续 BI 公式的方式完全相同,差异只在于如何定义 \(z_d(t)\)。 + +二者的核心区别在于优先采用哪类证据。观测锚定版本以诊断是否真实发生作为主要证据: +\[ + z^{\mathrm{obs}}_d(t)=1 + \quad\text{一旦疾病 } d \text{ 在 } t \text{ 前被观测到。} +\] +这适用于希望 BI 尽量贴近 HFRM 等诊断代码负担指标的场景。它透明、可审计,并且不容易受到模型校准误差影响; +但它会把同一种历史诊断都视为同等程度的已形成负担,不能表达严重程度、发生时间、上下文和残留影响的差异。 + +模型加权版本则以 DeepHealth 的历史风险轨迹作为主要证据: +\[ + z^{\mathrm{model}}_d(t) + = + 1-\prod_i + \left[ + 1-p_d\!\left(h(t_i),t_{i+1}-t_i\right) + \right]. +\] +该版本允许同一种已发生疾病在不同个体、不同时间和不同上下文下具有不同负担强度。例如,extra-info、疾病序列背景和隐含状态轨迹 +都会影响模型给出的区间风险。它可能更接近状态依赖的连续负担强度,但如果模型对某个真实发生过的疾病给出较低历史概率, +也可能低估该诊断事实对应的负担。 + +因此,二者回答的是相关但不完全相同的问题: +\[ + z^{\mathrm{obs}}_d(t): + \text{疾病 } d \text{ 是否已经作为诊断历史的一部分被记录?} +\] +\[ + z^{\mathrm{model}}_d(t): + \text{模型隐含轨迹在多大程度上支持疾病 } d \text{ 已经形成负担?} +\] +因此,这两个版本都应作为有价值的敏感性分析方案。观测锚定版本更适合强调可审计性和与既有代码指标的一致性; +模型加权版本更适合将 DeepHealth 作为连续状态模型,让学习到的历史轨迹调节疾病负担强度。 + +\subsection{累计强度形式} + +定义区间累计风险强度 +\[ + \ell_d(h_i,\Delta_i) + = + -\log\left[1-p_d(h_i,\Delta_i)\right], +\] +以及历史累计强度 +\[ + \Lambda^{\mathrm{model}}_d(t)=\sum_{i=0}^{n}\ell_d(h_i,\Delta_i). +\] +则 +\[ + z^{\mathrm{model}}_d(t)=1-\exp[-\Lambda^{\mathrm{model}}_d(t)]. +\] + +对于 exponential 模型, +\[ + \ell_d(h_i,\Delta_i)=\lambda_d(h_i)\Delta_i, +\] +因此 +\[ + z^{\mathrm{model}}_d(t) + = + 1-\exp\left[ + -\sum_{i=0}^{n} + \lambda_d\!\left(h(t_i)\right)(t_{i+1}-t_i) + \right]. +\] + +对于 Weibull 模型, +\[ + \ell_d(h_i,\Delta_i) + = + \lambda_d(h_i)\Delta_i^{\rho_d(h_i)}, +\] +因此 +\[ + z^{\mathrm{model}}_d(t) + = + 1-\exp\left[ + -\sum_{i=0}^{n} + \lambda_d\!\left(h(t_i)\right) + (t_{i+1}-t_i)^{\rho_d(h(t_i))} + \right]. +\] + +\section{未来预期负担} + +所选择的已形成负担 \(z_d(t)\) 表示截至查询时刻 \(t\) 已经形成的疾病负担。它可以是观测锚定负担 +\(z^{\mathrm{obs}}_d(t)\),也可以是模型加权负担 \(z^{\mathrm{model}}_d(t)\)。从当前查询时刻 \(t\) 出发, +未来时间窗 \(\tau\) 内的疾病风险为 +\[ + p_d(h(t),\tau). +\] +疾病 \(d\) 的未来预期新增负担定义为 +\[ + f_d(t,\tau) + = + \left[1-z_d(t)\right]p_d(h(t),\tau). +\] +该项只计算尚未形成的疾病负担部分,避免历史负担与未来风险重复计数。疾病 \(d\) 的总动态负担贡献为 +\[ + b_d(t,\tau) + = + z_d(t)+f_d(t,\tau). +\] +等价地, +\[ + b_d(t,\tau) + = + 1- + \left[1-z_d(t)\right] + \left[1-p_d(h(t),\tau)\right]. +\] +因此,\(b_d(t,\tau)\) 可以解释为疾病 \(d\) 的负担在时刻 \(t\) 已经形成,或将在未来时间窗 \(\tau\) 内新形成的累计概率。 + +\section{负担指数定义} + +设 \(A \in \mathbb{R}_{\ge 0}^{K \times D}\) 是疾病到负担维度的映射矩阵。维度 \(k\) 的历史负担、未来负担和总负担分别为 +\[ + \operatorname{BI}^{\mathrm{hist}}_k(t) + = + \sum_{d=1}^{D} A_{k,d} z_d(t), +\] +\[ + \operatorname{BI}^{\mathrm{future}}_k(t,\tau) + = + \sum_{d=1}^{D} + A_{k,d} + \left[1-z_d(t)\right]p_d(h(t),\tau), +\] +以及 +\[ + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + = + \operatorname{BI}^{\mathrm{hist}}_k(t) + + + \operatorname{BI}^{\mathrm{future}}_k(t,\tau). +\] +等价地, +\[ + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + = + \sum_{d=1}^{D} + A_{k,d} + \left\{ + 1- + \left[1-z_d(t)\right] + \left[1-p_d(h(t),\tau)\right] + \right\}. +\] + +对于 OBI,\(A=A^{\mathrm{organ}}\)。对于 FBI,\(A=A^{\mathrm{func}}\)。 + +\section{映射矩阵构建} + +\subsection{器官映射矩阵} + +器官映射矩阵应来自代码分类体系或经过验证的临床分组,而不是人为随意指定权重。在当前 ICD token 设置下, +第一版可以使用 ICD 章节或预定义 ICD 范围构建稀疏的疾病到器官 mask: +\[ + M^{\mathrm{organ}}_{k,d}\in\{0,1\}. +\] +示例包括: +\begin{center} +\begin{tabular}{ll} +\toprule +维度 & 示例 ICD 组 \\ +\midrule +心脏/血管 & I00--I99,可进一步拆分为心脏和血管组 \\ +脑/神经 & G00--G99,F00--F09,I60--I69 \\ +肾脏/泌尿生殖 & N00--N39,尤其 N17--N19 \\ +肺/呼吸 & J00--J99 \\ +代谢/内分泌 & E00--E90 \\ +肝脏/消化 & K00--K93,尤其 K70--K77 \\ +肌骨 & M00--M99 \\ +\bottomrule +\end{tabular} +\end{center} + +最简单的器官权重为 +\[ + A^{\mathrm{organ}}_{k,d}=M^{\mathrm{organ}}_{k,d}. +\] +如果有纵向器官终点标签,可以在 mask 约束下学习权重: +\[ + A^{\mathrm{organ}}_{k,d}\ge 0, + \qquad + A^{\mathrm{organ}}_{k,d}=0 + \quad\text{if}\quad + M^{\mathrm{organ}}_{k,d}=0. +\] +这样可以保持投影的临床可解释性,同时允许数据驱动校准。 + +\subsection{功能映射矩阵} + +功能映射矩阵应尽可能锚定在经过验证的衰弱相关诊断代码表上。CIHI-HFRM 或相关的 Hospital Frailty Risk Measure +是合适起点,因为它从诊断代码及其权重定义衰弱负担。 + +设 \(w^{\mathrm{HFRM}}_d\ge 0\) 是映射到 DeepHealth 疾病 token \(d\) 的 HFRM 权重。如果 HFRM 代码比 +DeepHealth 的 ICD token 更细,而 DeepHealth 使用三位 ICD token,则可按代码前缀聚合。保守默认方式是 +\[ + w^{\mathrm{HFRM}}_d + = + \max_{c:\, c \text{ maps to token } d} + w^{\mathrm{HFRM}}_c. +\] + +对于总功能负担,一维映射为 +\[ + A^{\mathrm{func,total}}_{1,d} + = + w^{\mathrm{HFRM}}_d. +\] +对于分域功能负担,定义分组 mask +\[ + G_{k,d}\in\{0,1\}, +\] +其中 \(G_{k,d}=1\) 表示 HFRM 相关疾病 token \(d\) 属于功能负担维度 \(k\)。于是 +\[ + A^{\mathrm{func}}_{k,d} + = + G_{k,d} w^{\mathrm{HFRM}}_d. +\] + +候选功能维度包括行动、认知、情绪、感官、营养、感染或免疫脆弱性、功能依赖和共病负担。在没有直接功能测量校准之前, +这些维度应被解释为诊断风险驱动的功能负担代理,而不是直接的功能储备测量。 + +\section{归一化与报告} + +原始负担指数是加权加和: +\[ + \operatorname{BI}_k(t,\tau) + = + \sum_d A_{k,d} b_d(t,\tau). +\] +为了可解释性,系统应报告三项分解: +\[ + \operatorname{BI}^{\mathrm{hist}}_k(t), + \qquad + \operatorname{BI}^{\mathrm{future}}_k(t,\tau), + \qquad + \operatorname{BI}^{\mathrm{total}}_k(t,\tau). +\] + +也可以报告归一化负担: +\[ + \widetilde{\operatorname{BI}}_k(t,\tau) + = + \frac{ + \sum_d A_{k,d} b_d(t,\tau) + }{ + \sum_d A_{k,d} + \epsilon + }, +\] +其中 \(\epsilon>0\) 用于避免除零。当 \(b_d(t,\tau)\in[0,1]\) 且 \(A_{k,d}\ge 0\) 时,该分数便于不同维度之间比较。 + +在队列层面,还可以在年龄和性别分层参考人群中计算相对分位数: +\[ + \operatorname{PercentileBI}_k(t,\tau) + = + \operatorname{rank}_{\mathrm{age,sex}} + \left( + \operatorname{BI}^{\mathrm{total}}_k(t,\tau) + \right). +\] +该分位数表示相对负担排名,而不是健康储备百分比。 + +\section{验证} + +OBI 与 FBI 应使用不同结局进行验证。 + +OBI 应针对器官系统特异性未来事件验证。例如,心脏/血管负担对应心血管事件,脑/神经负担对应卒中或痴呆, +肾脏负担对应 CKD 进展,肺部负担对应呼吸系统事件。 + +FBI 应针对 CIHI-HFRM 风格的衰弱诊断负担、衰弱相关诊断终点、住院、死亡、照护依赖代理指标, +或在条件允许时针对直接功能结局验证。如果没有 ADL/IADL、步速、握力、认知测验或恢复能力等直接功能标签, +FBI 应被报告为基于诊断风险的功能负担代理,而不是直接的功能储备测量。 + +\section{总结} + +DeepHealth 负担指数将疾病级风险预测转换为可解释的负担表征。已形成负担可以定义为观测锚定负担 +\(z^{\mathrm{obs}}_d(t)\),即遵循真实历史诊断;也可以定义为模型加权负担 \(z^{\mathrm{model}}_d(t)\), +即沿历史隐含状态轨迹累积 DeepHealth 预测的区间风险。未来预期负担则是在尚未形成的疾病负担部分上计算未来新增风险。 +OBI 使用解剖系统分组描述病理负担集中在哪里;FBI 使用 CIHI-HFRM 风格的衰弱相关诊断权重描述功能脆弱性负担。 +二者提供疾病负担的两种互补视角,同时允许明确选择已形成负担的语义。 + +\end{document}