Add burden index computation interfaces

This commit is contained in:
2026-06-25 17:15:56 +08:00
parent 0138cbd95b
commit a3bf3a0cd4
3 changed files with 1614 additions and 0 deletions

547
burden_index.py Normal file
View File

@@ -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

576
burden_index_method.tex Normal file
View File

@@ -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}

491
burden_index_method_zh.tex Normal file
View File

@@ -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--G99F00--F09I60--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}