临床 Python 进阶路线图
⌕ /
路线图 › 第四阶段 · AI 与工程化

第 15 章 · AI 辅助编程与 SAS→Python 代码迁移

本章目标:把大语言模型(LLM)变成你的日常生产力工具, 尤其是用它加速 SAS → Python 的迁移。 这是当前行业最真实、最有效的 AI 落地场景。


15.1 先看清 AI 在临床编程里能做什么、不能做什么

行业共识(PharmaSUG 2026 ET-223 的原话是 "AI could generate programming, but programmers who know the language efficiently handle AI"):

能做好 做不好 / 有风险
✅ 解释一段陌生的 SAS / Python 代码 ❌ 保证逻辑完全等价(必须人工核对)
✅ 语法翻译(DATA 步 → pandas) ❌ 理解你的 SAP 里的业务规则
✅ 生成样板代码(程序头、循环、批处理) ❌ 边界条件(>= 还是 >)
✅ 写单元测试与文档 ❌ 知道哪个"看起来对"的结果其实是错的
✅ 排查报错(把 Traceback 丢给它) ❌ 在 GxP 环境下直接交付未验证的代码
✅ 生成假数据用于开发测试 ❌ 处理受控术语/编码(MedDRA/WHODrug)

🔥 一句话原则: AI 负责"写得快",你负责"写得对"。 临床编程的最终责任永远在人身上。任何 AI 生成的代码都必须走完整的验证流程。


15.2 高效提示词:三个真正有用的模板

模板 1:SAS → Python 逐句翻译(最高频)

文本
你是一位精通 SAS 和 Python 的临床统计程序员,熟悉 CDISC SDTM/ADaM 标准。

请把下面的 SAS 代码转换成等价的 Python(pandas)代码。

要求:
1. 逐句对应,用注释标明每一行 Python 对应哪一行 SAS。
2. 使用向量化写法,不要用 for 循环遍历行。
3. 变量名保持全大写(CDISC 惯例)。
4. 特别说明以下容易出错的地方(如果有):
   - 缺失值处理(SAS 的 . 与 pandas 的 NaN/NaT 差异)
   - 舍入规则(SAS 是四舍五入,Python 是银行家舍入)
   - 排序与分组顺序(SAS 的 CLASS 顺序 vs pandas 的 Categorical)
   - 字符空串与缺失的区别
5. 最后给出一段"结果核对建议":我应该检查哪些数字来确认转换正确。

SAS 代码:
```sas
<粘贴你的 SAS 代码>

数据上下文:<变量清单 / 数据集行数 / 关键取值>

文本

### 模板 2:报错诊断(最省时间)

我在用 pandas 处理临床数据时遇到报错。请诊断原因并给出修复方案。

【环境】Python 3.x + pandas 2.x 【我的代码】

Python
<你的代码>

【报错信息】

文本
<Traceback 全文>

【数据结构】 【我的意图】<你想做什么>

请: 1. 用一句话说明根本原因。 2. 给出修复后的代码,并说明改了什么。 3. 告诉我以后如何避免同类问题。

文本

### 模板 3:代码审查(QC 视角)

请以"临床统计编程 QC 人员"的视角审查下面这段 Python 代码, 找出可能导致结果与方法学不一致的问题。

重点检查: 1. 分母是否正确(分析人群、受试者数 vs 事件数) 2. 缺失值是否被意外排除或意外计入 3. 舍入规则是否与 SAS 一致 4. 分组/排序顺序是否可复现 5. 连接(merge)是否可能产生重复或丢失记录 6. 是否有静默错误(不报错但结果错)

【代码】<粘贴> 【对应的 SAP 要求】<粘贴相关段落>

SAS

> 💡 **模板 3 的价值极高**:它相当于给你配了一个
> "永远不疲倦的 QC 伙伴"。虽然不能替代正式 QC,
> 但能在你提交前抓掉大部分低级错误。

---

## 15.3 实战演练:把一段真实 SAS 迁移过去

### 输入:一段典型的 SAS 安全性汇总

```sas
/* 计算各组受试者中,发生 SAE 的比例 */
proc sort data=adsl; by usubjid; run;
proc sort data=adae; by usubjid; run;

data sae;
    merge adsl (in=a) adae (in=b);
    by usubjid;
    if a;
    if aeser = 'Y' then SAEFL = 'Y';
run;

proc sql;
    create table sae_sum as
    select  trt01p,
            count(distinct usubjid) as N_SAE
    from    sae
    where   SAEFL = 'Y'
    group by trt01p;
quit;

proc sql;
    create table denom as
    select  trt01p,
            count(distinct usubjid) as N_TOT
    from    adsl
    where   saffl = 'Y'
    group by trt01p;
quit;

data final;
    merge sae_sum denom;
    by trt01p;
    PCT = N_SAE / N_TOT * 100;
    format PCT 5.1;
run;

输出:AI 迁移结果(附核对点)

Python
import numpy as np
import pandas as pd

# --- 对应 proc sort + merge adsl(in=a) adae; by usubjid; if a; ---
# 注意:SAS 的 merge 在这个场景下会"按 USUBJID 展开",
#       a=1 但无 AE 的受试者仍会被保留(AE 变量为空)
merged = pd.merge(adsl, adae, on="USUBJID", how="left", suffixes=("", "_AE"))

# --- 对应 if aeser = 'Y' then SAEFL = 'Y'; ---
# 注意 1:pandas 里字符缺失是 NaN,不是 '',用 == 'Y' 判断即可
# 注意 2:SAS 的 if 不给 SAEFL 赋缺失时它保持为空白;pandas 需要显式给 'N' 或 NaN
merged["SAEFL"] = np.where(merged["AESER"] == "Y", "Y", np.nan)

# --- 对应第一个 proc sql(分子)---
# ★ 关键点:SAS 用 count(distinct usubjid),所以要去重
#   而且这里的分母应该是"安全性人群",不是"全部 adsl"(原 SAS 用了 adsl 全集)
sae_n = (merged.query("SAEFL == 'Y'")
               .groupby("TRT01P")["USUBJID"].nunique()
               .rename("N_SAE"))

# --- 对应第二个 proc sql(分母)---
denom = (adsl.query("SAFFL == 'Y'")
             .groupby("TRT01P")["USUBJID"].nunique()
             .rename("N_TOT"))

# --- 对应 data final ---
final = (pd.concat([sae_n, denom], axis=1)
           .fillna({"N_SAE": 0})
           .reset_index())
final["PCT"] = (final["N_SAE"] / final["N_TOT"] * 100).round(1)

print(final.to_string(index=False))

必须核对的点(AI 未必全部指出)

核对项 为什么重要 怎么查
how="left" 对不对 SAS 的 if a = 保留 adsl 全部 = left join 比较行数与 USUBJID 数
分子分母的口径 原 SAS 分母用 adsl 全集;但 SAFFL='Y' 才有意义 这里原 SAS 可能有 bug! 需确认
count(distinct) pandas 用 nunique() 而不是 size() 同一受试者多条 SAE 时数字会不同
SAE 的数量 试点数据里 AESER='Y' 只有 3 条记录 (adae["AESER"]=="Y").sum()
输出顺序 治疗组顺序必须与 SAP 一致 用 Categorical

🔥 注意上面第 2 行:AI 转换时可能帮你发现原 SAS 代码的 bug。 例子中的原代码用 adsl 全集算分母,但变量叫 N_TOT 且加了 where saffl='Y' —— 两处口径不一致。这正是"知道语言的人才能有效使用 AI"的含义。


15.4 用 LLM 加速的其他高价值场景

场景 1:批量生成程序骨架与文档

Python
"""用 LLM 从统计需求生成程序头注释与骨架代码"""

PROMPT = """你是临床统计程序员。请为下面的 TLF 生成一段 SAS/Python 程序头注释。

输出要求:只输出注释块,包含字段:
项目/研究、程序名、程序用途、输入数据集、输出、创建人、创建日期、修订历史。

TLF 信息:
{tlf_info}
"""

def gen_program_header(client, tlf_info: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": PROMPT.format(tlf_info=tlf_info)}],
        temperature=0.2,
    )
    return resp.choices[0].message.content

场景 2:生成测试用假数据

Python
FAKE_DATA_PROMPT = """生成一段 Python 代码,造一个模拟的 SDTM AE 数据集用于开发测试。

要求:
- 200 个受试者,USUBJID 格式为 '01-{site:03d}-{subj:04d}'
- 每个受试者 0~8 条 AE
- 含变量:USUBJID, AESEQ, AETERM, AEDECOD, AEBODSYS, AESEV, AESER, AESTDTC, AEENDTC
- AESEV 只能是 MILD/MODERATE/SEVERE,分布约为 65%/30%/5%
- AESTDTC 在 2018-01-01 到 2020-12-31 之间
- 用 numpy 的 default_rng(seed) 保证可复现
- 输出纯代码,不要解释
"""

⚠️ 重要合规提醒:绝对不要把真实的患者数据、 未公开的研究数据、公司内部代码放进公开的 LLM 服务。 使用前必须确认: 1. 公司/客户是否允许使用外部 LLM 服务; 2. 数据是否已充分去标识化; 3. 是否有本地部署(如 Ollama + 开源模型)可用。 在临床环境里,这通常比技术问题更重要。

场景 3:给代码加注释与文档

文本
请为下面的函数补充 NumPy 风格的 docstring,
并逐行添加中文注释说明它在临床数据处理中的作用。
注意:注释要解释"为什么这么做",而不只是"这行做了什么"。

15.5 关于"AI 自动迁移 SAS→R/Python"的行业现状

PharmaSUG 2026 有几篇论文直接讨论这个:

论文 内容 链接
AI-305 Merck Improving AI SAS-to-R Code Migration via an Intermediate Design Document Layer:用"中间设计文档"作为 AI 迁移的桥梁,提升迁移准确性 PDF
AI-332 Merck A Human-in-the-Loop AI-Assisted Framework for ADaM Standardization:人在环中的 AI 辅助 ADaM 标准化框架 PDF
SS-261 TUM Closing the Loop: Validating AI-Generated SDTM Mappings using CDISC CORE and Synthetic Data PDF
ET-223 Servier Python as a Cost-Effective Solution for Clinical Statistical Programming(完整 Python TFL 工作流) PDF

从这些论文能提炼出的行业共识:

  1. 纯 AI 迁移不可靠,需要"中间表示"(设计文档/规格)来约束。
  2. 人在环中(Human-in-the-Loop)是当前最现实的架构。
  3. 验证要靠自动化工具(如 CDISC CORE)而不是靠人眼。
  4. "懂语言的程序员"是 AI 时代更值钱而不是更不值钱。

💡 PharmaSUG 2026 AI-305 的"中间设计文档层"思路值得直接借鉴: 不要直接把 SAS 丢给 AI 要 Python,而是先让 AI 把 SAS 的意图 写成一份结构化的设计说明(输入、人群、派生规则、统计方法、输出格式), 你审核这份说明无误后,再让 AI 按说明写 Python。 多一步,但准确性提升明显,而且这份说明本身就是很好的 QC 依据。


15.6 工具清单

用途 工具 说明
对话式编程 Claude / ChatGPT / 国内大模型 主力
IDE 内置补全 GitHub Copilot、Cursor、Trae、Qoder 你已在用多款 AI 编辑器
本地/离线模型 Ollama + Qwen / DeepSeek 数据不出内网
代码解释 直接把代码贴给 LLM 读别人代码效率翻倍
报错诊断 把 Traceback 贴给 LLM 省掉大量搜索时间
结构化迁移 先让 LLM 写设计文档,再写代码 见 15.5

15.7 动手练习

  1. 迁移练习:用 15.2 的模板 1,把一段你自己的 SAS 程序 (20-40 行)交给 LLM 迁移,然后按 15.3 的核对清单逐项验证。 记录 AI 出错的地方——这比成功案例更有价值。

  2. 报错诊断:故意写一段有错的 pandas 代码(比如不带括号的 & 条件、 忘记 na=False),把报错交给 LLM,看你能否从它的回答里学到新东西。

  3. QC 审查:把你写的一段 TFL 代码用模板 3 让 LLM 审查, 看它能否发现"分母口径"或"受试者去重"的问题。

  4. 假数据:用 15.4 场景 2 的提示词生成一份假的 AE 数据集, 用于测试你的 AE 汇总表脚本。

  5. (合规思考) 列出你所在环境使用 LLM 的三条红线, 以及为规避这些红线你打算采取的措施。


上一章 ← 第 14 章 · 统计分析与可视化 下一章 → 第 16 章 · Agent 开发入门:从 API 调用到临床 QC Agent