本章目标:把 Python 环境跑起来(15 分钟),并完成最关键的一步—— 理解 Python 与你熟悉的 SAS 在执行模型上的根本差异。
1.1 环境搭建
为什么 Python 需要"环境"而 SAS 不需要
这是新手最困惑、也最容易踩坑的地方。差别来自分发模型:
| SAS | Python | |
|---|---|---|
| 软件本体 | 商业软件,一次安装,全公司统一版本 | 开源,任何人都能发布"包" |
| 第三方库 | 主要是 SAS 官方 + 少量宏 | 有 50 万+ 个第三方包,版本互相依赖 |
| 冲突问题 | 基本不存在 | 普遍存在(A 包要 numpy 1.x,B 包要 2.x) |
| 解决方案 | 无 | 虚拟环境(venv / conda) |
一句话结论:虚拟环境就是给每个项目配一个"SAS 版本 + 授权库"的独立沙箱, 避免项目之间互相污染。每个项目一个环境,这是硬规矩。
推荐方案:官方 Python + venv(本项目采用)
如果你还没装 Python:
- 到 https://www.python.org/downloads/ 下载 Python 3.12 或 3.13 (临床场景建议不要用刚发布的奇数版本,等生态跟上)。
- 安装时务必勾选
Add Python to PATH。 - 安装完成后在终端验证:
python --version
# 期望输出:Python 3.12.x 或 3.13.x
关于 Anaconda:Anaconda 自带 pandas/numpy/Jupyter,开箱即用,
PharmaSUG 的论文里也常推荐它。但它体积大(3–5 GB)、会污染全局环境。
本教程用更轻的 venv 方案;如果你的公司统一用 conda,把下面的
venv 换成 conda create 即可,后续内容完全一致。
三步建立本项目环境
# 1) 进入项目目录
cd clinical-python-roadmap
# 2) 创建虚拟环境(会在当前目录生成 .venv 文件夹)
python -m venv .venv
# 3) 激活环境
# Windows PowerShell:
.venv\Scripts\Activate.ps1
# Windows CMD:
.venv\Scripts\activate.bat
# macOS / Linux:
source .venv/bin/activate
# 激活成功后,命令行提示符前会出现 (.venv)
⚠️ Windows PowerShell 常见报错:
无法加载文件 ... Activate.ps1,因为在此系统上禁止运行脚本解决:以管理员身份运行 PowerShell,执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned,然后重试。
安装依赖
# 确保已激活虚拟环境,然后:
pip install -r requirements.txt
# 验证
python -c "import pandas, numpy, pyreadstat; print('OK', pandas.__version__)"
requirements.txt 内容(本项目):
pandas>=2.2 # 数据操作核心,等价于 DATA 步 + PROC SQL + PROC MEANS
numpy>=1.26 # 数值计算与向量化
pyreadstat>=1.2 # 读取 SAS 的 .xpt / .sas7bdat(关键!不用装 SAS)
matplotlib>=3.8 # 绘图
openpyxl>=3.1 # 读写 Excel
requests>=2.31 # HTTP 请求(Agent 章节要用)
pytest>=8.0 # 单元测试
编辑器:VS Code(推荐)
临床程序员从 SAS Enterprise Guide / Display Manager 迁移过去,VS Code 的对应关系是:
| SAS 里的东西 | VS Code 里的对应物 |
|---|---|
| 资源管理器 | 左侧 Explorer 面板 |
| 提交代码 (F8 / Run) | 右上角 ▶ 按钮,或 Shift+Enter 执行当前单元格 |
| 日志窗口 | 底部 TERMINAL / JUPYTER 面板 |
| 语法高亮 | 内置,安装 Python 扩展后更强 |
| 断点调试 | 行号左侧点击打断点,F5 启动调试 |
| 程序编辑器 | .py 文件,或用 Jupyter Notebook(.ipynb,交互式,类似 EG) |
必装扩展:Python(Microsoft)、Jupyter(Microsoft)、Pylance、Ruff。
💡 建议:学习阶段用 Jupyter Notebook,因为它像 SAS 的 EG—— 一段一段执行、结果即时可见、变量常驻内存; 但最终交付的脚本一定要是
.py,因为那才是可版本管理、可自动化的形态。
1.2 第一个程序:不装 SAS 也能读 SAS 数据
在你敲完 pip install 之后,立刻做一件事——用 Python 打开一个真实的
临床数据集。这个即时反馈非常重要。
# file: hello_clinical.py
import pyreadstat
# 读取 CDISC 试点项目的 SDTM DM 域(受试者人口学)
# data/samples/ 里已备好转换后的 CSV;如果下载了 XPT,也可以直接读 .xpt
df, meta = pyreadstat.read_xport("data/raw/dm.xpt")
print(f"行数 = {df.shape[0]},列数 = {df.shape[1]}")
print(f"前 5 个变量:{list(df.columns[:5])}")
print(df[["USUBJID", "AGE", "SEX", "RACE"]].head())
print(f"\n变量标签:AGE -> {meta.column_names_to_labels['AGE']}")
如果你已经运行了 python scripts/download_data.py --core,输出会是:
行数 = 306,列数 = 25
前 5 个变量:['STUDYID', 'DOMAIN', 'USUBJID', 'SUBJID', 'RFSTDTC', 'RFENDTC', 'RFXSTRT']
USUBJID AGE SEX RACE
0 01-701-1015 63.0 F WHITE
1 01-701-1023 64.0 M WHITE
2 01-701-1028 71.0 M WHITE
...
变量标签:AGE -> Age
注意几个和 SAS 不同的点:
df.shape返回(行数, 列数),等价于PROC CONTENTS里的观测数与变量数。- 变量名在 pandas 里是列名的字符串;需要用
df["AGE"]这种写法取,而不是AGE。 - 变量标签还在(
meta.column_names_to_labels),这是 pyreadstat 帮你从 XPT 里带出来的。
1.3 思维转换:SAS 与 Python 的五个根本差异
这一段比任何语法都重要。不完成这层转换,你会写出"用 Python 语法写的 SAS"。
差异 1:语句 vs 表达式——谁在干活?
/* SAS:过程作用在数据集上,数据集是被动的 */
proc means data=adsl n mean std;
var AGE;
class TRT01P;
run;
# Python:数据自己会做事(方法调用),主语变了
adsl.groupby("TRT01P")["AGE"].agg(["count", "mean", "std"])
| SAS | Python | |
|---|---|---|
| 语法形状 | 动词 数据; 选项; |
数据.动作(参数) |
| 主体 | PROC 是主体,数据集是宾语 | 数据是主体,方法是动作 |
| 思维 | "我要对 a 做 MEANS" | "让 a 自己按组算均值" |
记忆口诀:SAS 是"过程驱动",Python 是"对象驱动"。
差异 2:全局环境 vs 局部作用域
SAS 里你几乎永远在操作一个全局的 WORK 库;
data 语句的输出数据集默认留在 WORK 里,下个步骤就能用。
data adsl2; set adsl; where SAFFL='Y'; run;
proc means data=adsl2; run; /* adsl2 还在 WORK 里 */
Python 里没有自动留存的中间数据集——你必须在变量里显式接住结果:
adsl2 = adsl[adsl["SAFFL"] == "Y"] # 必须赋值给变量
adsl2.groupby("TRT01P").size() # 然后用这个变量
🧠 这是最容易踩的坑:
adsl.dropna()写完发现数据没变—— 因为它返回新对象,不修改原对象。必须写adsl = adsl.dropna()或adsl.dropna(inplace=True)。
差异 3:缺失值不是"空白",是多套机制
| 场景 | SAS | Python |
|---|---|---|
| 数值缺失 | .(特殊数值) |
np.nan(浮点 NaN)或 pd.NA |
| 字符缺失 | ''(空串,SAS 里空串 = 缺失) |
None 或 np.nan(空串 '' 不等于缺失!) |
| 判断 | if age = . |
pd.isna(age) 或 age != age |
| 比较特性 | SAS 自动把 . 视为最小 |
np.nan 参与任何比较都是 False |
import numpy as np
np.nan == np.nan # False !这在 SAS 里是不可思议的
np.nan < 10 # False
np.isnan(np.nan) # True —— 正确的判断方式
关键教训:Python 里永远不要用 == 判断缺失,用 pd.isna()。
另外,SAS 里 if name = '' then 能捕获缺失字符,Python 里空串是合法的非缺失值,
必须清洗时显式处理 "" 和 NaN 两种情况。详见第 10 章。
差异 4:逐行 vs 向量化——性能的量级差异
/* SAS 的 DATA 步天然逐行:这是它的优势,简单直观 */
data want;
set have;
if age >= 65 then agegrp = '>=65';
else agegrp = '<65';
run;
# 写法 A:逐行循环("用 Python 写 SAS")——慢,且不推荐
for i in range(len(df)):
df.loc[i, "AGEGRP"] = ">=65" if df.loc[i, "AGE"] >= 65 else "<65"
# 写法 B:向量化(Python 的正道)——快百倍,代码更短
df["AGEGRP"] = np.where(df["AGE"] >= 65, ">=65", "<65")
# 写法 C:多条件(对应 SAS 的 select/when 或嵌套 if)
df["AGEGRP"] = np.select(
[df["AGE"] < 65, df["AGE"] <= 80],
["<65", "65-80"],
default=">80",
)
在 306 行数据上你感觉不到差别;在 58,700 行的 ADLBC 上, 循环写法要几秒,向量化是毫秒级。第 06 章整章都在训练这个思维。
差异 5:日志自动滚动 vs 异常立即中断
| SAS | Python | |
|---|---|---|
| 出错表现 | 日志里出现 ERROR/WARNING,程序继续跑完(或按选项停) | 抛异常,程序在该行中断 |
| 排查方式 | 肉眼看日志 | 读 Traceback(从下往上读!) |
| 主动输出 | %put |
print() / logging |
| 需要"继续跑" | 默认行为 | 必须 try/except 显式处理 |
# SAS 思维:想检查一行就 put,然后继续
# Python 思维:直接让程序崩,读 Traceback 定位
try:
df = pd.read_sas("data/raw/not_exist.xpt", format="xport")
except FileNotFoundError:
print("文件不存在,请先运行 download_data.py")
读 Traceback 的方法(这是新手最需要练的技能):
Traceback (most recent call last):
File "case02.py", line 47, in <module> ← 最后一行才是真正出错的位置
tbl = df.groupby("TRT01P")["AGEGR1"].value_counts()
File ".../pandas/core/groupby.py", line 1234, in ...
KeyError: 'AGEGRP1' ← 最后一行是错误原因:列名拼错了
读法:从最下面一行往上读。最后一行告诉你"什么错", 倒数第二段告诉你"错在自己代码的哪一行"。中间那些是库的内部调用,先忽略。
1.4 SAS 与 Python 的"环境对照速查"
| 概念 | SAS | Python |
|---|---|---|
| 库 / 目录 | LIBNAME adsl 'C:\data'; |
普通文件夹路径 / pathlib.Path |
| 数据集 | adsl.sas7bdat |
DataFrame 对象(内存)+ CSV/Parquet/xpt(磁盘) |
| 一次分析单元 | 一个 .sas 程序 |
一个 .py 脚本 / 一个 .ipynb 笔记本 |
| 跨程序共享 | 永久数据集 / 宏变量 | 模块 import / 函数返回值 / 中间文件 |
| 版本管理 | 通常无(或手写 _v2.sas) |
Git(第 17 章) |
| 复用代码 | %INCLUDE / %MACRO |
import 自己的 .py 模块 |
| 环境隔离 | 基本不需要 | 虚拟环境(必须) |
| 官方文档 | support.sas.com | docs.python.org + pandas.pydata.org |
1.5 动手练习
- 完成环境搭建,
python -c "import pandas; print(pandas.__version__)"能输出版本号。 - 运行
python scripts/download_data.py --core下载数据(或在data/samples/里用 CSV)。 - 运行 1.2 节的
hello_clinical.py,确认输出 306 行。 - (思维转换练习) 把下面这段 SAS 用 Python 写出来,先不管优雅与否:
sas data adsl2; set adsl; if AGE >= 65 then AGEGRP = '>=65'; else AGEGRP = '<65'; if SAFFL = 'Y'; run;提示:你需要一个np.where(或列表推导式)和一个布尔索引。
1.6 常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
python 不是内部或外部命令 |
安装时未勾选 Add to PATH | 重装并勾选,或手动加 PATH |
ModuleNotFoundError: No module named 'pandas' |
忘了激活虚拟环境 / 装到了别的环境 | 看命令行前面有没有 (.venv) |
Activate.ps1 无法加载 |
PowerShell 执行策略 | Set-ExecutionPolicy -Scope CurrentUser RemoteSigned |
| 中文输出乱码 | Windows 控制台编码 | 在脚本首行加 # -*- coding: utf-8 -*-;或 PYTHONUTF8=1 |
pip install 极慢 |
默认源在国外 | pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple |
读取 XPT 报 does not exist |
路径写法不对(尤其 Git Bash 的 /tmp) |
用 Windows 绝对路径 r"G:\...\dm.xpt",或 pathlib.Path |
上一章 ← 第 00 章 · 学习路线图 下一章 → 第 02 章 · 基础语法速通