mcpField
R
rwd-cohort-analysis

rwd-cohort-analysis

github.com/awslabs数据与数据库6880 次安装4 周前更新
代码仓库

关于 rwd-cohort-analysis

面向真实世界数据研究的分析技能,为使用理赔或 EHR 数据的研究者提供从队列构建到生存分析的可复现流程。涵盖用药依从性、倾向评分与平衡诊断,并提供 Python 和 R 代码;数据访问和代码表验证由用户负责。

主要功能

  1. 01按诊断、操作和药物代码定义队列
  2. 02计算 PDC 与 MPR 用药依从性
  3. 03估计倾向评分并诊断协变量平衡
  4. 04执行 IPTW、匹配及加权分析
  5. 05绘制 Kaplan-Meier 曲线并拟合 Cox 模型
  6. 06检验比例风险假设

适用场景

  1. 01基于理赔或 EHR 数据识别研究队列
  2. 02比较治疗组的用药依从性
  3. 03调整协变量后估计治疗效果
  4. 04检查生存模型的比例风险假设

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/awslabs/hcls-agent-skills --skill rwd-cohort-analysis

安装后在 Claude Code 中输入/rwd-cohort-analysis确认已加载。

详细介绍

真实世界数据队列分析流水线

概述

此技能提供一套可复现的流水线,用于分析行政理赔数据或 EHR 数据,以估计治疗比较效果。涵盖从原始理赔表到校正后的生存估计的完整工作流:

  1. 使用诊断(ICD-10-CM)、操作(CPT/HCPCS)和药物(NDC)代码列表识别队列
  2. 测量用药依从性(PDC 和 MPR)
  3. 估计倾向评分并诊断协变量平衡
  4. Kaplan-Meier 曲线和 Cox 比例风险回归

代码片段使用 Python(pandas、scikit-learn、lifelines)和 R (MatchIt、cobalt、survival、survminer)。用户负责获取数据、遵守 IRB/隐私要求,以及验证临床代码列表。

不在范围内:从源系统提取数据、对临床记录进行自然语言处理、中断时间序列分析、工具变量分析。

用法

在用户要求以下操作时调用:

  • 使用诊断/操作/药物代码从理赔或 EHR 数据构建队列
  • 计算用药依从性的 PDC 或 MPR
  • 估计倾向评分并检查协变量平衡
  • 对基于理赔数据构建的队列运行 Kaplan-Meier 分析或 Cox 回归
  • 评估比例风险假设

此技能会输出准确代码。用户需提供数据和代码列表。

响应格式

  • 先给出用户需要的命令或代码,再作解释
  • 按以下顺序组织:确认输入 → 可运行代码 → 解释关键参数 → 注意事项
  • 每项任务提供一个完整可运行的示例;不要展示所有替代方案
  • 代码注释应精简且实用(说明做什么,而非为何存在)
  • 目标:50–100 行代码,并附简短说明

核心概念

数据模型假设

此流水线预期使用以下表格(列名仅供示例):

表格关键列
enrollmentpatient_id、enroll_start、enroll_end
diagnosespatient_id、dx_date、icd10_code、dx_position
procedurespatient_id、proc_date、cpt_code
pharmacypatient_id、fill_date、ndc_code、days_supply、quantity
demographicspatient_id、birth_date、sex、race
1. 队列识别
定义代码列表
# ICD-10-CM codes for type 2 diabetes (example)
T2D_ICD10 = ["E11.0", "E11.1", "E11.2", "E11.3", "E11.4",
             "E11.5", "E11.6", "E11.8", "E11.9"]

# NDC codes for Drug A (user supplies actual NDCs)
DRUG_A_NDC = ["12345678901", "12345678902"]
DRUG_B_NDC = ["98765432101", "98765432102"]

# CPT codes for outcome procedure (e.g., amputation)
OUTCOME_CPT = ["27590", "27591", "27592"]
筛选有洗脱期的新用药者
import pandas as pd

def identify_new_users(pharmacy, ndc_list, washout_days=180):
    """Identify incident users: first fill after washout with no prior fills."""
    drug_fills = pharmacy[pharmacy["ndc_code"].isin(ndc_list)].copy()
    drug_fills = drug_fills.sort_values(["patient_id", "fill_date"])

    # First-ever fill
    first_fill = drug_fills.groupby("patient_id")["fill_date"].min().reset_index()
    first_fill.columns = ["patient_id", "index_date"]

    # Require continuous enrollment for washout_days before index
    cohort = first_fill.merge(enrollment, on="patient_id")
    cohort["washout_start"] = cohort["index_date"] - pd.Timedelta(days=washout_days)
    cohort = cohort[cohort["enroll_start"] <= cohort["washout_start"]]

    # Exclude patients with any fill of the drug during washout
    cohort = cohort.merge(drug_fills, on="patient_id", suffixes=("", "_rx"))
    prior_fills = cohort[
        (cohort["fill_date"] >= cohort["washout_start"]) &
        (cohort["fill_date"] < cohort["index_date"])
    ]
    prevalent_users = prior_fills["patient_id"].unique()
    cohort = first_fill[~first_fill["patient_id"].isin(prevalent_users)]
    return cohort  # columns: patient_id, index_date
应用纳入/排除标准
def apply_criteria(cohort, diagnoses, enrollment, min_age=18,
                   required_dx_codes=None, excluded_dx_codes=None,
                   pre_index_window=365, min_followup=30):
    """Filter cohort by age, required/excluded diagnoses, enrollment."""
    # Age at index
    cohort = cohort.merge(demographics, on="patient_id")
    cohort["age"] = (cohort["index_date"] - cohort["birth_date"]).dt.days / 365.25
    cohort = cohort[cohort["age"] >= min_age]

    # Required diagnosis in pre-index window
    if required_dx_codes:
        pre_dx = diagnoses[diagnoses["icd10_code"].isin(required_dx_codes)]
        pre_dx = pre_dx.merge(cohort[["patient_id", "index_date"]], on="patient_id")
        pre_dx = pre_dx[
            (pre_dx["dx_date"] >= pre_dx["index_date"] - pd.Timedelta(days=pre_index_window)) &
            (pre_dx["dx_date"] < pre_dx["index_date"])
        ]
        cohort = cohort[cohort["patient_id"].isin(pre_dx["patient_id"].unique())]

    # Exclude patients with certain diagnoses
    if excluded_dx_codes:
        excl = diagnoses[diagnoses["icd10_code"].isin(excluded_dx_codes)]
        excl = excl.merge(cohort[["patient_id", "index_date"]], on="patient_id")
        excl = excl[excl["dx_date"] < excl["index_date"]]
        cohort = cohort[~cohort["patient_id"].isin(excl["patient_id"].unique())]

    # Minimum follow-up enrollment
    cohort = cohort.merge(enrollment, on="patient_id")
    cohort["followup_end"] = cohort[["enroll_end", cohort.columns[-1]]].min(axis=1)
    cohort = cohort[
        (cohort["enroll_end"] - cohort["index_date"]).dt.days >= min_followup
    ]
    return cohort
2. 用药依从性
PDC(覆盖天数比例)

PDC 是首选指标(由 PQA 认可)。它统计测量期内至少有一次处方覆盖的天数,最高按 1.0 计。

def calculate_pdc(pharmacy, cohort, ndc_list, period_days=365):
    """Calculate PDC for each patient over a fixed measurement period."""
    fills = pharmacy[pharmacy["ndc_code"].isin(ndc_list)].copy()
    fills = fills.merge(cohort[["patient_id", "index_date"]], on="patient_id")
    fills["period_end"] = fills["index_date"] + pd.Timedelta(days=period_days)

    # Keep fills within measurement period
    fills = fills[fills["fill_date"] < fills["period_end"]]
    fills["cover_start"] = fills["fill_date"]
    fills["cover_end"] = fills["fill_date"] + pd.to_timedelta(fills["days_supply"], unit="D")
    fills["cover_end"] = fills[["cover_end", "period_end"]].min(axis=1)
    fills["cover_start"] = fills[["cover_start", "index_date"]].max(axis=1)

    # Build day-level coverage (vectorized per patient)
    results = []
    for pid, grp in fills.groupby("patient_id"):
        days_covered = set()
        for _, row in grp.iterrows():
            days_covered.update(pd.date_range(row["cover_start"], row["cover_end"] - pd.Timedelta(days=1)))
        results.append({"patient_id": pid, "pdc": min(len(days_covered) / period_days, 1.0)})
    return pd.DataFrame(results)
MPR(药物持有比例)
def calculate_mpr(pharmacy, cohort, ndc_list, period_days=365):
    """MPR = total days supply dispensed / days in period. Can exceed 1.0."""
    fills = pharmacy[pharmacy["ndc_code"].isin(ndc_list)].copy()
    fills = fills.merge(cohort[["patient_id", "index_date"]], on="patient_id")
    fills["period_end"] = fills["index_date"] + pd.Timedelta(days=period_days)
    fills = fills[(fills["fill_date"] >= fills["index_date"]) &
                  (fills["fill_date"] < fills["period_end"])]
    mpr = fills.groupby("patient_id")["days_supply"].sum().reset_index()
    mpr.columns = ["patient_id", "total_supply"]
    mpr["mpr"] = mpr["total_supply"] / period_days
    return mpr
指标公式范围备注
PDC覆盖天数 / 时段天数0–1.0首选;重叠处方不会重复计数
MPR总供药天数 / 时段天数0–∞提前续药时可能超过 1.0;优先级较低

依从性阈值:PDC ≥ 0.80 是“依从”的标准界值。

3. 倾向评分估计
Python(scikit-learn)
from sklearn.linear_model import LogisticRegression
import numpy as np

def estimate_ps(cohort, covariate_cols, treatment_col="treatment"):
    """Fit logistic regression PS model. Returns cohort with ps column."""
    X = cohort[covariate_cols].fillna(0)
    y = cohort[treatment_col]
    model = LogisticRegression(max_iter=1000, solver="lbfgs", C=1e6)
    model.fit(X, y)
    cohort = cohort.copy()
    cohort["ps"] = model.predict_proba(X)[:, 1]
    return cohort, model
R(MatchIt + cobalt)
library(MatchIt)
library(cobalt)

# Estimate PS and perform 1:1 nearest-neighbor matching
m <- matchit(treatment ~ age + sex + charlson + prior_hosp + prior_rx_count,
             data = cohort, method = "nearest", distance = "glm",
             caliper = 0.2, ratio = 1)
matched_data <- match.data(m)

# Balance diagnostics
bal.tab(m, thresholds = c(m = 0.1))  # SMD threshold
love.plot(m, threshold = 0.1)
4. 平衡性诊断
SMD 计算(Python)
def compute_smd(cohort, covariate_cols, treatment_col="treatment"):
    """Compute standardized mean differences for all covariates."""
    treated = cohort[cohort[treatment_col] == 1]
    comparator = cohort[cohort[treatment_col] == 0]
    results = []
    for col in covariate_cols:
        mean_t = treated[col].mean()
        mean_c = comparator[col].mean()
        var_t = treated[col].var()
        var_c = comparator[col].var()
        pooled_sd = np.sqrt((var_t + var_c) / 2)
        smd = abs(mean_t - mean_c) / pooled_sd if pooled_sd > 0 else 0
        results.append({"covariate": col, "smd": round(smd, 4),
                        "balanced": smd < 0.1})
    return pd.DataFrame(results)
诊断项目标未达标时的处理
SMD所有协变量均 < 0.1在 PS 模型中加入交互项/非线性项
方差比0.5–2.0重新设定 PS 模型或截除极端值
PS 重叠两组之间有较大重叠截除不重叠区域;报告截除后的 N
权重分布(IPTW)均值 ≈ 1.0;最大值 < 10在第 1/99 百分位处截断
5. IPTW(逆概率治疗加权)
def compute_iptw(cohort, treatment_col="treatment", ps_col="ps",
                 stabilized=True, truncate_pct=(1, 99)):
    """Compute stabilized IPTW weights with truncation."""
    cohort = cohort.copy()
    t = cohort[treatment_col]
    ps = cohort[ps_col]

    if stabilized:
        p_treat = t.mean()
        cohort["iptw"] = np.where(t == 1, p_treat / ps, (1 - p_treat) / (1 - ps))
    else:
        cohort["iptw"] = np.where(t == 1, 1 / ps, 1 / (1 - ps))

    # Truncate extreme weights
    lo, hi = np.percentile(cohort["iptw"], truncate_pct)
    cohort["iptw"] = cohort["iptw"].clip(lo, hi)
    return cohort
6. Kaplan-Meier 生存分析
Python(lifelines)
from lifelines import KaplanMeierFitter
import matplotlib.pyplot as plt

def plot_km(cohort, duration_col="followup_days", event_col="event",
            group_col="treatment", labels=("Drug B", "Drug A")):
    """Plot Kaplan-Meier curves by treatment group."""
    fig, ax = plt.subplots(figsize=(8, 6))
    kmf = KaplanMeierFitter()
    for grp, label in zip([0, 1], labels):
        mask = cohort[group_col] == grp
        kmf.fit(cohort.loc[mask, duration_col], cohort.loc[mask, event_col],
                label=label)
        kmf.plot_survival_function(ax=ax, ci_show=True)
    ax.set_xlabel("Days from index date")
    ax.set_ylabel("Survival probability")
    ax.set_title("Kaplan-Meier Survival Curves")
    plt.tight_layout()
    return fig
R(survival + survminer)
library(survival)
library(survminer)

fit <- survfit(Surv(followup_days, event) ~ treatment, data = cohort)
ggsurvplot(fit, data = cohort, pval = TRUE, risk.table = TRUE,
           xlab = "Days from index date", ylab = "Survival probability",
           legend.labs = c("Drug B", "Drug A"))
7. Cox 比例风险模型
Python(lifelines)
from lifelines import CoxPHFitter

def fit_cox(cohort, duration_col="followup_days", event_col="event",
            covariates=None, weight_col=None):
    """Fit Cox PH model, optionally weighted (IPTW)."""
    cols = [duration_col, event_col] + (covariates or [])
    if weight_col:
        cols.append(weight_col)
    df = cohort[cols].dropna()
    cph = CoxPHFitter()
    cph.fit(df, duration_col=duration_col, event_col=event_col,
            weights_col=weight_col, robust=True if weight_col else False)
    cph.print_summary()
    return cph
R(survival)
# Unweighted (matched cohort)
cox_fit <- coxph(Surv(followup_days, event) ~ treatment + age + sex + charlson,
                 data = matched_data)
summary(cox_fit)

# IPTW-weighted
cox_iptw <- coxph(Surv(followup_days, event) ~ treatment,
                  data = cohort, weights = iptw, robust = TRUE)
summary(cox_iptw)
8. 比例风险假设检验
Schoenfeld 残差(Python)
def check_ph_assumption(cph):
    """Test and plot Schoenfeld residuals for PH assumption."""
    cph.check_assumptions(cohort, p_value_threshold=0.05, show_plots=True)
R
ph_test <- cox.zph(cox_fit)
print(ph_test)       # p < 0.05 → PH assumption violated for that covariate
plot(ph_test)         # visual: residuals should show no trend over time
PH 检验结果操作
所有 p > 0.05PH 假设成立;继续
协变量 p < 0.05对该协变量进行分层,或添加时间交互项
全局 p < 0.05考虑限制平均生存时间(RMST)或参数 AFT 模型
流程总结
1. Define code lists (ICD-10, NDC, CPT)
2. Identify new users with washout          → cohort table
3. Apply inclusion/exclusion criteria       → filtered cohort
4. Build baseline covariate matrix          → pre-index features
5. Estimate propensity scores               → ps column
6. Match or weight (IPTW)                   → balanced cohort
7. Check balance (SMD < 0.1)               → Love plot / SMD table
8. Calculate adherence (PDC/MPR)            → adherence metrics
9. Define outcome + follow-up               → duration + event columns
10. Kaplan-Meier curves                     → survival plot
11. Cox PH model (adjusted or weighted)     → hazard ratios + CIs
12. Check PH assumption (Schoenfeld)        → assumption diagnostics

常见错误

  • 错误: 使用 MPR 而非 PDC 衡量药物依从性 正确: 使用 PDC(覆盖天数比例)作为依从性指标 原因: MPR 会对重叠配药重复计数,数值可能超过 1.0,且未获 PQA 认可

  • 错误: 未要求研究期间连续参保 正确: 确认洗脱期和随访期内均连续参保 原因: 参保中断会产生无法观测的时段,导致遗漏事件和配药记录

  • 错误: 未在索引日期前设置洗脱期 正确: 要求有一段干净的洗脱期(通常为 180 天),期间未配过研究药物 原因: 没有洗脱期,既往用药者会混入新用药者队列,导致结果偏倚

  • 错误: 在倾向评分模型中纳入索引日期后的协变量 正确: PS 模型中仅使用索引日期前的(基线)变量 原因: 索引日期后的变量可能受治疗影响,从而引入碰撞偏倚

  • 错误: 匹配或加权后用 p 值评估协变量平衡 正确: 对所有协变量使用标准化均值差(SMD < 0.1) 原因: p 值混淆了平衡程度与样本量,因此不适用于此目的

  • 错误: 未检查治疗组之间的倾向评分重叠情况 正确: 绘制两组的 PS 分布图,并截去不重叠区域 原因: 对不重叠区域的估计依赖外推,可靠性较低

  • 错误: 未检验比例风险假设就报告 Cox 模型结果 正确: 始终运行 Schoenfeld 残差检验,并检查图中是否存在时间趋势 原因: PH 假设违反会导致风险比产生误导;应考虑 RMST 或分层

  • 错误: 在 IPTW 加权分析中使用朴素标准误 正确: 拟合加权模型时使用稳健(三明治)方差估计量 原因: 朴素 SE 会忽略权重估计步骤,从而低估不确定性

  • 错误: 过度截尾 IPTW 权重(例如截在第 5/95 百分位) 正确: 谨慎截尾(第 1/99 百分位),并报告不同阈值下的敏感性分析 原因: 过度截尾实际上会取消关键观测的加权,从而重新引入混杂

  • 错误: 将所有患者的随访结束时间定义为固定的日历日期 正确: 在以下最早发生的时间结束随访:事件、退保、研究结束或死亡 原因: 不正确的删失会使生存估计产生偏倚,并违反非信息性删失假设

参考文献

评论

支持 Markdown