| from __future__ import annotations |
|
|
| from typing import Any |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| from sepsis_mcp.constants import STATIC_COLUMNS |
| from sepsis_mcp.dataset import build_labeled_patient_frame |
|
|
|
|
| def _build_window_matrices( |
| patient_frame: pd.DataFrame, |
| sample_index: int, |
| dynamic_columns: list[str], |
| lookback_hours: int, |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: |
| values = np.zeros((lookback_hours, len(dynamic_columns)), dtype=np.float32) |
| masks = np.zeros((lookback_hours, len(dynamic_columns)), dtype=np.float32) |
| deltas = np.zeros((lookback_hours, len(dynamic_columns)), dtype=np.float32) |
|
|
| window_start = max(0, sample_index - lookback_hours + 1) |
| actual_window = patient_frame.iloc[window_start : sample_index + 1] |
| pad_length = lookback_hours - len(actual_window) |
|
|
| for feature_offset, column in enumerate(dynamic_columns): |
| series = actual_window[column].tolist() |
| for time_offset, value in enumerate(series, start=pad_length): |
| if pd.notna(value): |
| values[time_offset, feature_offset] = float(value) |
| masks[time_offset, feature_offset] = 1.0 |
|
|
| if time_offset == 0: |
| deltas[time_offset, feature_offset] = 0.0 |
| elif masks[time_offset, feature_offset] == 1.0: |
| deltas[time_offset, feature_offset] = 0.0 |
| else: |
| previous_delta = deltas[time_offset - 1, feature_offset] |
| deltas[time_offset, feature_offset] = previous_delta + 1.0 |
|
|
| return values, masks, deltas |
|
|
|
|
| def _global_missing_rate(masks: np.ndarray) -> float: |
| return float(1.0 - masks.mean()) |
|
|
|
|
| def build_patient_grud_samples( |
| patient_frame: pd.DataFrame, |
| patient_id: str, |
| dynamic_columns: list[str], |
| lookback_hours: int = 12, |
| horizon_hours: int = 6, |
| ) -> list[dict[str, Any]]: |
| labeled = build_labeled_patient_frame( |
| patient_frame, |
| patient_id=patient_id, |
| horizon_hours=horizon_hours, |
| ) |
|
|
| samples: list[dict[str, Any]] = [] |
| for _, row in labeled.iterrows(): |
| sample_index = int(row["sample_index"]) |
| values, masks, deltas = _build_window_matrices( |
| patient_frame, |
| sample_index=sample_index, |
| dynamic_columns=dynamic_columns, |
| lookback_hours=lookback_hours, |
| ) |
| current_row = patient_frame.iloc[sample_index] |
| static = np.array( |
| [ |
| float(current_row[column]) if pd.notna(current_row[column]) else 0.0 |
| for column in STATIC_COLUMNS |
| ], |
| dtype=np.float32, |
| ) |
| samples.append( |
| { |
| "patient_id": patient_id, |
| "sample_index": sample_index, |
| "values": values, |
| "masks": masks, |
| "deltas": deltas, |
| "static": static, |
| "label": int(row["target_in_6h"]), |
| "global_missing_rate": _global_missing_rate(masks), |
| } |
| ) |
|
|
| return samples |
|
|
|
|
| def stack_grud_samples(samples: list[dict[str, Any]]) -> dict[str, Any]: |
| return { |
| "values": np.stack([sample["values"] for sample in samples]).astype(np.float32), |
| "masks": np.stack([sample["masks"] for sample in samples]).astype(np.float32), |
| "deltas": np.stack([sample["deltas"] for sample in samples]).astype(np.float32), |
| "static": np.stack([sample["static"] for sample in samples]).astype(np.float32), |
| "labels": np.array([sample["label"] for sample in samples], dtype=np.float32), |
| "global_missing_rates": np.array( |
| [sample["global_missing_rate"] for sample in samples], |
| dtype=np.float32, |
| ), |
| "patient_ids": [sample["patient_id"] for sample in samples], |
| "sample_indices": [sample["sample_index"] for sample in samples], |
| } |
|
|
|
|
| def fit_grud_scaler(stacked: dict[str, Any]) -> dict[str, np.ndarray]: |
| values = stacked["values"] |
| masks = stacked["masks"] |
| static = stacked["static"] |
|
|
| dynamic_mean = np.zeros(values.shape[-1], dtype=np.float32) |
| dynamic_std = np.ones(values.shape[-1], dtype=np.float32) |
| for feature_index in range(values.shape[-1]): |
| observed = values[:, :, feature_index][masks[:, :, feature_index] == 1] |
| if observed.size: |
| dynamic_mean[feature_index] = np.float32(observed.mean()) |
| feature_std = np.float32(observed.std()) |
| dynamic_std[feature_index] = feature_std if feature_std > 0 else 1.0 |
|
|
| static_mean = np.nanmean(static, axis=0).astype(np.float32) |
| static_std = np.nanstd(static, axis=0).astype(np.float32) |
| static_std[static_std == 0] = 1.0 |
|
|
| return { |
| "dynamic_mean": dynamic_mean, |
| "dynamic_std": dynamic_std, |
| "static_mean": static_mean, |
| "static_std": static_std, |
| } |
|
|
|
|
| def transform_grud_stacked( |
| stacked: dict[str, Any], |
| scaler: dict[str, np.ndarray], |
| ) -> dict[str, Any]: |
| transformed = dict(stacked) |
| values = stacked["values"].copy() |
| masks = stacked["masks"] |
|
|
| for feature_index in range(values.shape[-1]): |
| observed = masks[:, :, feature_index] == 1 |
| values[:, :, feature_index][observed] = ( |
| values[:, :, feature_index][observed] - scaler["dynamic_mean"][feature_index] |
| ) / scaler["dynamic_std"][feature_index] |
| values[:, :, feature_index][~observed] = 0.0 |
|
|
| static = stacked["static"].copy() |
| static = np.where(np.isfinite(static), static, scaler["static_mean"]) |
| static = (static - scaler["static_mean"]) / scaler["static_std"] |
|
|
| transformed["values"] = values.astype(np.float32) |
| transformed["static"] = static.astype(np.float32) |
| return transformed |
|
|