File size: 3,825 Bytes
7787c15
 
 
 
 
 
 
 
 
f666f82
 
 
 
 
7787c15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f666f82
 
f570757
7787c15
 
 
f570757
 
 
 
f666f82
 
 
 
 
 
7787c15
 
 
 
 
 
f666f82
7787c15
 
 
f666f82
 
 
7787c15
 
 
 
 
 
 
 
 
 
 
f570757
7787c15
 
e61da8d
7787c15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""DM ๊ฐœ์ธ๋ณ„ ์œ„ํ—˜์š”์ธ - local SHAP.

LLM_Report/utils/dm_risk.py์™€ ๊ณ„์‚ฐ ๋กœ์ง์€ ๊ฐ™๋‹ค(5-fold ์ฒดํฌํฌ์ธํŠธ ๊ฐ๊ฐ์—
๋Œ€ํ•ด KernelExplainer๋กœ local SHAP์„ ๊ตฌํ•˜๊ณ  ํ‰๊ท  - ์žฌํ•™์Šต ์—†์Œ). ๋‹ค๋ฅธ ์ ์€
๋ฐฐ๊ฒฝํ‘œ๋ณธ์„ ์›๋ณธ ๋ฐ์ดํ„ฐ์—์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ kmeans ์š”์•ฝํ•˜์ง€ ์•Š๊ณ 
WEB/scripts/precompute_artifacts.py๊ฐ€ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋‘” dm_shap_background.csv
(kmeans ํ•ฉ์„ฑ ๋Œ€ํ‘œ์  100๊ฐœ, ์‹ค์ œ ๊ฐœ์ธ ๋ ˆ์ฝ”๋“œ ์•„๋‹˜)๋ฅผ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค๋Š” ๊ฒƒ -
์›๋ณธ ๋ฐ์ดํ„ฐ๋ฅผ Space์— ์•ˆ ์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•จ์ด์ž, ๋งค ์š”์ฒญ๋งˆ๋‹ค kmeans๋ฅผ ๋‹ค์‹œ ์•ˆ
๋Œ๋ ค๋„ ๋˜๋ฏ€๋กœ ๋” ๋น ๋ฅด๋‹ค(๋ฌด๋ฃŒ CPU์—์„œ ํŠนํžˆ ์ค‘์š”).

์ฒดํฌํฌ์ธํŠธ๋Š” PRS_DM์„ ๋„ฃ์–ด ํ•™์Šตํ•œ ๋ชจ๋ธ(ML/checkpoints/dm_prs)์ด๋‹ค. prs_value
(z ์ ์ˆ˜)๋ฅผ ์ฃผ๋ฉด PRS๋ฅผ mask=1๋กœ ๋„ฃ๊ณ  PRS_DM๋„ ํ•˜๋‚˜์˜ SHAP feature๋กœ ์„ค๋ช…ํ•œ๋‹ค.
None์ด๋ฉด PRS๋ฅผ [MASK]๋กœ ๋‘๊ณ  EHR feature๋งŒ ์„ค๋ช…ํ•œ๋‹ค(ํ•™์Šต ๋•Œ --prs-dropout์œผ๋กœ
[MASK]๋„ ํ•™์Šต๋ผ ์žˆ์–ด EHR๋งŒ์œผ๋กœ ์˜ˆ์ธก ๊ฐ€๋Šฅ).
"""
from pathlib import Path

import numpy as np
import pandas as pd
import shap
import torch

from models.mlp import MultimodalModel
from utils.shap_utils import build_predict_fn


def compute_dm_local_shap(
    patient_row: dict,
    features: list,
    background_df: pd.DataFrame,
    checkpoint_dir: str = "artifacts/checkpoints",
    n_folds: int = 5,
    prs_dim: int = 1,
    device: str = "cpu",
    top_k: int = 8,
    prs_value: float = None,
    prs_feature: str = "PRS_DM",
    exclude_features: tuple = (),
) -> tuple:
    """๋ฐ˜ํ™˜: (importance_df(top_k, feature/patient_value/local_shap/direction),
    ensemble_predicted_prob). 5๊ฐœ fold ์ฒดํฌํฌ์ธํŠธ ๊ฐ๊ฐ์œผ๋กœ ์ด ํ•œ ์‚ฌ๋žŒ์˜ local
    SHAP์„ ๊ตฌํ•˜๊ณ  feature๋ณ„ ํ‰๊ท ์„ ์ตœ์ข…๊ฐ’์œผ๋กœ ์“ด๋‹ค(์˜ˆ์ธก ํ™•๋ฅ ๋„ 5๊ฐœ ๋ชจ๋ธ ํ‰๊ท ).
    exclude_features: ์˜ˆ์ธก์—๋Š” ๊ทธ๋Œ€๋กœ ์“ฐ๋˜ ์„ค๋ช… ํ‘œ์—์„œ๋งŒ ๋บ„ feature - ์‚ฌ์šฉ์ž๊ฐ€
    ์ž…๋ ฅํ•˜์ง€ ์•Š์•„ ์ธ๊ตฌ ์ค‘๊ฐ„๊ฐ’์œผ๋กœ ์ฑ„์šด ํ•ญ๋ชฉ์€ ์ค‘๊ฐ„๊ฐ’์ด SHAP ๋ฐฐ๊ฒฝ ํ‰๊ท ๊ณผ ๋‹ฌ๋ผ
    (์˜ˆ: ALT ์ค‘๊ฐ„๊ฐ’ 19 vs ํ‰๊ท  22.7) ์ž…๋ ฅํ•˜์ง€๋„ ์•Š์€ ๊ฐ’์— ๊ธฐ์—ฌ๋„๊ฐ€ ์ƒ๊ธฐ๋ฏ€๋กœ ๋บ€๋‹ค."""
    prs_in_input = prs_value is not None
    ehr_features = list(features)
    patient_row = dict(patient_row)
    if prs_in_input:
        features = ehr_features + [prs_feature]
        patient_row[prs_feature] = float(prs_value)
    patient_vec = np.array([[patient_row[f] for f in features]], dtype=np.float32)
    background = background_df[features].to_numpy(dtype=np.float32)

    shap_values_per_fold, probs = [], []
    for fold_id in range(n_folds):
        checkpoint_path = Path(checkpoint_dir) / f"dm_fold{fold_id}.pt"
        model = MultimodalModel(ehr_dim=len(ehr_features), prs_dim=prs_dim).to(device)
        model.load_state_dict(torch.load(checkpoint_path, map_location=device))
        model.eval()

        predict_fn = build_predict_fn(
            model, prs_dim=max(prs_dim, 1), device=device, apply_sigmoid=True, prs_in_input=prs_in_input
        )
        explainer = shap.KernelExplainer(predict_fn, background)
        sv = np.asarray(explainer.shap_values(patient_vec)).reshape(1, len(features))
        shap_values_per_fold.append(sv[0])
        probs.append(float(predict_fn(patient_vec)[0]))

    mean_shap = np.mean(shap_values_per_fold, axis=0)
    ensemble_prob = float(np.mean(probs))

    df = pd.DataFrame(
        {"feature": features, "patient_value": [patient_row[f] for f in features], "local_shap": mean_shap}
    )
    df = df[~df["feature"].isin(exclude_features)]
    df = df.assign(_abs=df["local_shap"].abs()).sort_values("_abs", ascending=False).drop(columns="_abs")
    df = df.head(top_k).reset_index(drop=True)
    df["direction"] = df["local_shap"].apply(lambda x: "๋‹น๋‡จ๋ณ‘ ์œ„ํ—˜ ์ฆ๊ฐ€" if x > 0 else "๋‹น๋‡จ๋ณ‘ ์œ„ํ—˜ ๊ฐ์†Œ")
    return df, ensemble_prob