Tool 4 - SR Chart Generator: - PRISMA 2020 flow diagram with Chinese/English toggle (SVG) - Baseline characteristics table (Table 1) - Dual data source: project pipeline API + Excel upload - SVG/PNG export support - Backend: ChartingService with Prisma aggregation - Frontend: PrismaFlowDiagram, BaselineTable, DataSourceSelector Tool 5 - Meta Analysis Engine: - 3 data types: HR (metagen), dichotomous (metabin), continuous (metacont) - Random and fixed effects models - Multiple effect measures: HR / OR / RR - Forest plot + funnel plot (base64 PNG from R) - Heterogeneity statistics: I2, Q, p-value, Tau2 - Data input via Excel upload or project pipeline - R Docker image updated with meta package (13 tools total) - E2E test: 36/36 passed - Key fix: exp() back-transformation for log-scale ratio measures Also includes: - IIT CRA Agent V3.0 routing and AI chat page integration - Updated ASL module status guide (v2.3) - Updated system status guide (v6.3) - Updated R statistics engine guide (v1.4) Tested: Frontend renders correctly, backend APIs functional, E2E tests passed Made-with: Cursor
42 KiB
R 统计引擎架构与部署指南
版本: v1.4
更新日期: 2026-02-26
维护者: SSA-Pro 开发团队 / ASL 循证工具箱团队
状态: ✅ 生产就绪(13 工具 + Block-based 标准化输出 — 新增 Meta 分析引擎)
📋 目录
- 概述
- 架构设计
- Docker 镜像构建
- 部署指南
- API 参考
- 开发指南
- 6.1 添加新工具(含 Block-based 模板)
- 6.5 各工具参数快速参考
- 6.6 R 语言陷阱速查(7 大坑)
- 6.7 开发环境新增 R 包
- 运维指南
- 常见问题
- 测试指南
1. 概述
1.1 什么是 R 统计引擎
R 统计引擎是平台的专用统计计算服务,基于 Docker 容器化部署,提供:
- 🧮 严谨的统计分析能力(T 检验、方差分析、回归等)
- 🛡️ 统计护栏(正态性检验、方差齐性检验等)
- 📊 可视化输出(Base64 编码的图表)
- 📝 可复现代码生成(APA 格式的 R 脚本)
1.2 定位
┌─────────────────────────────────────────────────────────────┐
│ 业务模块层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ SSA-Pro │ │ ASL │ │ 其他 │ │
│ │ 智能统计 │ │Meta分析 │ │ 模块 │ │
│ └────┬────┘ └────┬────┘ └─────────┘ │
├───────┼──────────┼──────────────────────────────────────────┤
│ ▼ ▼ 通用能力层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ R 统计引擎 (Docker) │ │
│ │ • /health 健康检查 │ │
│ │ • /api/v1/tools 工具列表 │ │
│ │ • /api/v1/skills 技能执行 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
1.3 技术栈
| 组件 | 版本 | 说明 |
|---|---|---|
| R | 4.3.3 | 统计计算核心 |
| plumber | 1.2.1 | REST API 框架 |
| ggplot2 | 最新 | 数据可视化 |
| car | 3.1-2 | 高级统计检验 |
| dplyr/tidyr | 最新 | 数据处理 |
| gtsummary | 最新 | 基线特征表生成(Phase Deploy 新增) |
| gt/broom | 最新 | 表格渲染/模型整理(Phase Deploy 新增) |
| scales/gridExtra | 最新 | 坐标轴格式化/多图排版(Phase Deploy 新增) |
| meta | 8.2.1 | Meta 分析引擎(ASL 工具 5 新增 — metagen/metabin/metacont) |
| Docker | 24+ | 容器化部署 |
2. 架构设计
2.1 Brain-Hand 模型
R 统计引擎采用 Brain-Hand 分离架构:
┌──────────────────┐ ┌──────────────────┐
│ Node.js │ │ R Docker │
│ (Brain) │ │ (Hand) │
├──────────────────┤ ├──────────────────┤
│ • 业务逻辑 │ HTTP │ • 统计计算 │
│ • 认证鉴权 │ ───────> │ • 数据处理 │
│ • OSS 签名 │ │ • 图表生成 │
│ • 结果解释 │ <─────── │ • 代码生成 │
└──────────────────┘ JSON └──────────────────┘
2.2 数据传输协议
支持两种数据传输方式:
| 方式 | 条件 | 字段 |
|---|---|---|
| inline | 数据 < 2MB | data_source.data (JSON) |
| oss | 数据 >= 2MB | data_source.oss_url (预签名 URL) |
// 方式 1: inline
{
"data_source": {
"type": "inline",
"data": [{"group": "A", "value": 10}, ...]
}
}
// 方式 2: oss (预签名 URL)
{
"data_source": {
"type": "oss",
"oss_url": "https://bucket.oss.com/data.csv?signature=xxx"
}
}
2.2.1 inline 数据格式详解
R 数据加载器 (utils/data_loader.R) 支持两种 JSON 数据格式:
| 格式 | 说明 | 示例 |
|---|---|---|
| 行格式 | JSON 对象数组,每个对象是一行 | [{"sex": 1, "age": 25}, {"sex": 2, "age": 30}] |
| 列格式 | JSON 对象,每个属性是一列 | {"sex": [1, 2], "age": [25, 30]} |
推荐:使用行格式,与 JavaScript/TypeScript 的数据处理习惯一致。
Node.js 调用示例:
// 推荐:行格式(Array of Objects)
const data = [
{ sex: 1, age: 25, bmi: 22.5 },
{ sex: 2, age: 30, bmi: 24.1 },
// ...
];
const response = await axios.post('http://localhost:8082/api/v1/skills/ST_T_TEST_IND', {
data_source: {
type: 'inline',
data: data // 直接传入数组
},
params: {
group_var: 'sex',
value_var: 'age'
}
});
2.3 安全设计
| 安全措施 | 实现方式 |
|---|---|
| 非特权用户 | USER appuser |
| 路径遍历防护 | tool_code 正则白名单 ^[A-Z][A-Z0-9_]*$ |
| OSS 密钥隔离 | Node.js 生成预签名 URL,R 无需持有密钥 |
| 健康检查 | Docker HEALTHCHECK |
3. Docker 镜像构建
3.1 完整 Dockerfile
FROM rocker/r-ver:4.3
LABEL maintainer="dev-team@aiclinicalresearch.com"
LABEL version="1.0.1"
LABEL description="SSA-Pro R Statistics Service"
# 安装系统依赖(包括 R 包编译所需的库)
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libsodium-dev \
zlib1g-dev \
libnlopt-dev \
liblapack-dev \
libblas-dev \
gfortran \
pkg-config \
cmake \
curl \
&& rm -rf /var/lib/apt/lists/*
# 直接安装 R 包(含 Phase Deploy + ASL Meta 分析依赖)
RUN R -e "install.packages(c( \
'plumber', \
'jsonlite', \
'ggplot2', \
'glue', \
'dplyr', \
'tidyr', \
'base64enc', \
'yaml', \
'car', \
'httr', \
'scales', \
'gridExtra', \
'gtsummary', \
'gt', \
'broom', \
'meta' \
), repos='https://cloud.r-project.org/', Ncpus=2)"
# 安全加固:创建非特权用户
RUN useradd -m -s /bin/bash appuser
WORKDIR /app
# 复制应用代码
COPY plumber.R plumber.R
COPY utils/ utils/
COPY tools/ tools/
COPY tests/ tests/
# 设置目录权限
RUN chown -R appuser:appuser /app
# 切换到非特权用户
USER appuser
EXPOSE 8080
# 环境变量
ENV DEV_MODE="false"
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动服务
CMD ["R", "-e", "plumber::plumb('plumber.R')$run(host='0.0.0.0', port=8080)"]
3.2 系统依赖说明
| 依赖包 | 用途 |
|---|---|
libcurl4-openssl-dev |
httr 包(HTTP 请求) |
libssl-dev |
openssl 包(加密) |
libxml2-dev |
xml2 包(XML 解析) |
libsodium-dev |
sodium 包(加密) |
zlib1g-dev |
httpuv 包(Web 服务器) |
libnlopt-dev |
nloptr 包(优化算法) |
liblapack-dev |
线性代数计算 |
libblas-dev |
基础线性代数 |
gfortran |
Fortran 编译器(部分 R 包需要) |
cmake |
nloptr 包构建 |
curl |
健康检查 |
3.3 构建命令
# 本地构建
cd r-statistics-service
docker build -t ssa-r-statistics:1.0.1 .
# 查看镜像
docker images ssa-r-statistics
# 预期输出
REPOSITORY TAG IMAGE ID CREATED SIZE
ssa-r-statistics 1.0.1 xxxxxxxxxxxx x minutes ago 1.81GB
3.4 构建时间参考
| 阶段 | 耗时 |
|---|---|
| 基础镜像下载 | ~2 分钟(首次) |
| 系统依赖安装 | ~1 分钟 |
| R 包安装(16 个,含 gtsummary/gt/meta) | ~15 分钟 |
| 总计 | ~17 分钟 |
4. 部署指南
4.1 开发环境
使用 docker-compose:
# r-statistics-service/docker-compose.yml
services:
ssa-r-service:
build: .
container_name: ssa-r-statistics
ports:
- "8082:8080" # 主机8082 → 容器8080(REDCap占用8080/8081)
environment:
- DEV_MODE=true
volumes:
# 开发环境挂载:支持热重载
- ./plumber.R:/app/plumber.R # ⚠️ 重要:API 入口也需要挂载
- ./tools:/app/tools
- ./utils:/app/utils
- ./tests:/app/tests
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
启动命令:
cd r-statistics-service
docker-compose up -d
4.1.1 热重载机制详解
| 文件类型 | 热重载支持 | 说明 |
|---|---|---|
tools/*.R |
✅ 自动 | DEV_MODE=true 时每次请求重新加载 |
utils/*.R |
⚠️ 需重启 | 服务启动时加载,修改后需 docker-compose restart |
plumber.R |
⚠️ 需重启 | API 路由定义,修改后需 docker-compose restart |
最佳实践:
- 开发新工具时,只需修改
tools/目录,无需重启 - 修改
utils/或plumber.R后,执行docker-compose restart - 添加新的 API 端点后,需要
docker-compose up -d --force-recreate
4.2 生产环境 (SAE)
# SAE 配置
容器镜像: registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.1
实例规格: 2 vCPU, 4 GB
最小实例: 1
最大实例: 5
端口: 8080
环境变量:
DEV_MODE: "false"
4.3 环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
DEV_MODE |
false |
开发模式(启用热重载,每次请求重新加载工具脚本) |
说明:开发环境和生产环境都使用真实 OSS,无需 Mock 数据。
- 开发环境:
ai-clinical-data-devbucket- 生产环境:
ai-clinical-databucket
4.4 端口配置
| 环境 | 主机端口 | 容器端口 | 说明 |
|---|---|---|---|
| 开发环境 | 8082 | 8080 | 避免与 REDCap 8080/8081 冲突 |
| 生产环境 (SAE) | 8080 | 8080 | 云端无端口冲突 |
注意:Node.js 后端通过
R_SERVICE_URL环境变量配置 R 服务地址,默认值为http://localhost:8082。
5. API 参考
5.1 健康检查
GET /health
响应:
{
"status": "ok",
"timestamp": "2026-02-19 08:00:00",
"version": "1.0.1",
"dev_mode": true,
"tools_loaded": 13
}
5.2 工具列表
GET /api/v1/tools
响应:
{
"status": "ok",
"tools": [
"anova_one",
"baseline_table",
"chi_square",
"correlation",
"descriptive",
"fisher",
"linear_reg",
"logistic_binary",
"mann_whitney",
"meta_analysis",
"t_test_ind",
"t_test_paired",
"wilcoxon"
],
"count": 13
}
已实现的统计工具(13 个)
Phase 2A 基础工具(7 个)
| tool_code | 名称 | 场景 |
|---|---|---|
ST_T_TEST_IND |
独立样本 T 检验 | 两组连续变量比较(正态) |
ST_MANN_WHITNEY |
Mann-Whitney U | 两组连续变量比较(非参数) |
ST_T_TEST_PAIRED |
配对 T 检验 | 前后对比 |
ST_CHI_SQUARE |
卡方检验 | 分类变量关联 |
ST_CORRELATION |
相关分析 | Pearson/Spearman 相关 |
ST_LOGISTIC_BINARY |
二元 Logistic 回归 | 多因素分析(二分类结局) |
ST_DESCRIPTIVE |
描述性统计 | 基线表、数据概况 |
Phase Deploy 新增工具(5 个)
| tool_code | 名称 | 场景 |
|---|---|---|
ST_FISHER |
Fisher 精确检验 | 小样本/稀疏列联表(卡方替代) |
ST_ANOVA_ONE |
单因素方差分析 | 三组及以上均值比较(含 Kruskal-Wallis 降级) |
ST_WILCOXON |
Wilcoxon 符号秩检验 | 配对非参数检验(配对 T 替代) |
ST_LINEAR_REG |
线性回归 | 连续结局多因素分析 |
ST_BASELINE_TABLE |
基线特征表(复合工具) | 基于 gtsummary 的一键式基线表生成 |
ASL 循证工具箱新增(1 个)
| tool_code | 名称 | 场景 | 调用方 |
|---|---|---|---|
ST_META_ANALYSIS |
Meta 分析引擎 | HR / 二分类 / 连续型 Meta 分析,生成森林图 + 漏斗图 | ASL 工具 5 |
跨模块复用说明:
ST_META_ANALYSIS由 ASL 模块(工具 5:Meta 分析引擎)引入,后端代理位于backend/src/modules/asl/meta-analysis/,使用inline数据协议(Meta 分析通常 5-30 个研究,数据量极小)。该工具同样可被 SSA 或其他模块复用。
5.3 执行技能
POST /api/v1/skills/{tool_code}
Content-Type: application/json
请求体:
{
"data_source": {
"type": "inline",
"data": [...]
},
"params": {
"group_var": "group",
"value_var": "value"
},
"guardrails": {
"check_normality": true
}
}
成功响应:
{
"status": "success",
"message": "分析完成",
"warnings": null,
"results": {
"method": "Welch Two Sample t-test",
"statistic": -5.196,
"df": 5.98,
"p_value": 0.002,
"p_value_fmt": "p = .002"
},
"plots": ["data:image/png;base64,..."],
"trace_log": [...],
"reproducible_code": "..."
}
错误响应:
{
"status": "error",
"error_code": "E001",
"error_type": "business",
"message": "列名 'xxx' 在数据中不存在",
"user_hint": "请检查变量名是否拼写正确"
}
5.4 JIT 护栏检查(Phase 2A 新增)
在执行核心统计工具前,调用此端点检验统计假设(正态性、方差齐性等)。
POST /api/v1/guardrails/jit
Content-Type: application/json
请求体:
{
"data_source": {
"type": "inline",
"data": [...]
},
"tool_code": "ST_T_TEST_IND",
"params": {
"group_var": "sex",
"value_var": "age"
}
}
响应:
{
"status": "success",
"checks": [
{
"check_name": "正态性检验 (组: 1)",
"passed": true,
"p_value": 0.234,
"recommendation": "满足正态性"
},
{
"check_name": "方差齐性检验 (Levene)",
"passed": false,
"p_value": 0.012,
"recommendation": "建议使用 Welch 校正"
}
],
"suggested_tool": "ST_MANN_WHITNEY",
"can_proceed": true,
"all_checks_passed": false
}
使用场景:
- 工作流执行器在调用核心统计方法前,先调用 JIT 护栏
- 根据
suggested_tool自动切换到更合适的方法 - 将
checks结果展示给用户
5.5 复合工具示例:基线特征表(Phase Deploy 新增)
POST /api/v1/skills/ST_BASELINE_TABLE
Content-Type: application/json
请求体:
{
"data_source": {
"type": "inline",
"data": [
{"group": "Drug", "age": 45, "sex": "M", "sbp": 130, "bmi": 24.5, "smoking": "Yes"},
{"group": "Placebo", "age": 47, "sex": "F", "sbp": 128, "bmi": 23.8, "smoking": "No"}
]
},
"params": {
"group_var": "group",
"analyze_vars": ["age", "sex", "sbp", "bmi", "smoking"]
}
}
成功响应(核心字段):
{
"status": "success",
"results": {
"n_total": 30,
"n_groups": 2,
"n_variables": 5,
"significant_vars": ["sbp"],
"method_info": [
{"variable": "age", "method": "Wilcoxon rank sum test"},
{"variable": "sex", "method": "Fisher's exact test"}
]
},
"report_blocks": [
{
"type": "table",
"headers": ["Characteristic", "Drug, N = 15", "Placebo, N = 15", "p-value"],
"rows": [["age", "49 (42, 55)", "47 (41, 54)", "0.6"]],
"title": "基线特征表 (按 group 分组)",
"metadata": { "is_baseline_table": true, "group_var": "group", "has_p_values": true }
}
]
}
特点:
ST_BASELINE_TABLE是复合工具,基于gtsummary::tbl_summary()自动判断变量类型(连续/分类)、选择统计方法(T 检验/Mann-Whitney/卡方/Fisher),输出标准三线表。report_blocks[0].metadata.is_baseline_table = true触发前端特殊渲染(P 值标星、rowspan 合并行)。
5.6 Meta 分析示例(ASL 工具 5 — v1.4 新增)
POST /api/v1/skills/ST_META_ANALYSIS
Content-Type: application/json
请求体(HR 数据类型):
{
"data_source": {
"type": "inline",
"data": [
{"study_id": "Gandhi 2018", "hr": 0.49, "lower_ci": 0.38, "upper_ci": 0.64},
{"study_id": "Socinski 2018", "hr": 0.56, "lower_ci": 0.45, "upper_ci": 0.70},
{"study_id": "West 2019", "hr": 0.60, "lower_ci": 0.45, "upper_ci": 0.80}
]
},
"params": {
"data_type": "hr",
"model": "random"
}
}
支持的 3 种数据类型:
| data_type | 必需列 | R 函数 | 效应指标 |
|---|---|---|---|
hr |
study_id, hr, lower_ci, upper_ci |
meta::metagen() |
HR |
dichotomous |
study_id, events_e, total_e, events_c, total_c |
meta::metabin() |
OR / RR / RD |
continuous |
study_id, mean_e, sd_e, n_e, mean_c, sd_c, n_c |
meta::metacont() |
MD |
params 参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data_type |
string | 必填 | "hr" / "dichotomous" / "continuous" |
model |
string | "random" |
"random" (DerSimonian-Laird) / "fixed" |
effect_measure |
string | 自动 | 仅二分类有效:"OR" / "RR" / "RD" |
成功响应(核心字段):
{
"status": "success",
"results": {
"pooled_effect": 0.5948,
"pooled_lower": 0.5255,
"pooled_upper": 0.6733,
"pooled_pvalue": 0.0,
"i_squared": 15.4,
"tau_squared": 0.0031,
"q_statistic": 4.73,
"q_pvalue": 0.316,
"k_studies": 3,
"effect_measure": "HR",
"model": "Random Effects"
},
"plots": [
"data:image/png;base64,...(森林图)",
"data:image/png;base64,...(漏斗图)"
],
"report_blocks": [
{"type": "key_value", "title": "Meta-Analysis Summary", "items": [...]},
{"type": "image", "title": "Forest Plot", "data": "..."},
{"type": "image", "title": "Funnel Plot", "data": "..."},
{"type": "markdown", "title": "Heterogeneity Assessment", "content": "..."}
]
}
重要实现细节 — 对数尺度反变换:
metaR 包对比值类效应量(HR / OR / RR)的内部计算在对数尺度上进行,即TE.random返回的是log(HR)而非 HR。meta_analysis.R在结果提取阶段对比值类指标自动执行exp()反变换,确保 API 返回临床可读的效应量(如 HR = 0.59 而非 log(HR) = -0.52)。连续型指标(MD)无需反变换。
6. 开发指南
6.1 添加新工具
- 在
tools/目录创建 R 脚本:
# tools/my_analysis.R
#' @tool_code ST_MY_ANALYSIS
#' @name 我的分析工具
#' @version 1.0.0
#' @description 工具描述
#' @author SSA-Pro Team
library(glue)
library(ggplot2)
library(base64enc)
run_analysis <- function(input) {
# ===== 初始化日志 =====
logs <- c()
log_add <- function(msg) { logs <<- c(logs, paste0("[", Sys.time(), "] ", msg)) }
# ===== 数据加载 =====
log_add("开始加载输入数据")
df <- tryCatch(
load_input_data(input),
error = function(e) {
log_add(paste("数据加载失败:", e$message))
return(NULL)
}
)
if (is.null(df)) {
return(make_error(ERROR_CODES$E100_INTERNAL_ERROR, details = "数据加载失败"))
}
log_add(glue("数据加载成功: {nrow(df)} 行, {ncol(df)} 列"))
# ===== 参数提取 =====
p <- input$params
my_var <- p$my_var
# ===== 参数校验 =====
if (!(my_var %in% names(df))) {
return(make_error(ERROR_CODES$E001_COLUMN_NOT_FOUND, col = my_var))
}
# ===== 护栏检查 =====
guardrail_results <- list()
warnings_list <- c()
sample_check <- check_sample_size(nrow(df), min_required = 10, action = ACTION_WARN)
guardrail_results <- c(guardrail_results, list(sample_check))
guardrail_status <- run_guardrail_chain(guardrail_results)
if (guardrail_status$status == "blocked") {
return(list(status = "blocked", message = guardrail_status$reason, trace_log = logs))
}
# ===== 核心计算 =====
log_add("执行分析...")
# result <- your_analysis_function(df, ...)
# ===== 构建 report_blocks(必须!) =====
blocks <- list()
# Block: 检验结果(key_value)
blocks[[length(blocks) + 1]] <- make_kv_block(
list("方法" = "Your Method", "统计量" = "1.234", "P 值" = format_p_value(0.05)),
title = "检验结果"
)
# Block: 图表(image)
plot_base64 <- tryCatch({
p <- ggplot(df, aes(x = df[[my_var]])) + geom_histogram() + theme_minimal()
tmp_file <- tempfile(fileext = ".png")
ggsave(tmp_file, p, width = 7, height = 5, dpi = 100)
base64_str <- base64encode(tmp_file)
unlink(tmp_file)
paste0("data:image/png;base64,", base64_str)
}, error = function(e) NULL)
if (!is.null(plot_base64)) {
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "分析图表")
}
# Block: 结论(markdown)
blocks[[length(blocks) + 1]] <- make_markdown_block("分析结论...", title = "结论摘要")
# ===== 生成可复现代码 =====
reproducible_code <- glue('
# SSA-Pro 自动生成代码
# 工具: 我的分析工具
# 时间: {Sys.time()}
# ================================
df <- read.csv("data.csv")
# 你的分析代码...
')
# ===== 返回结果 =====
log_add("分析完成")
return(list(
status = "success",
message = "分析完成",
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
results = list(
# 统计结果(使用 jsonlite::unbox 保证单值不被包装成数组)
statistic = jsonlite::unbox(1.234),
p_value = jsonlite::unbox(0.05),
p_value_fmt = format_p_value(0.05)
),
report_blocks = blocks, # ⚠️ 必须!前端 DynamicReport 依赖此字段渲染
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
trace_log = logs,
reproducible_code = as.character(reproducible_code)
))
}
-
开发模式:修改
tools/下的文件后,无需重启,下次请求自动加载 -
测试:
curl -X POST http://localhost:8082/api/v1/skills/ST_MY_ANALYSIS \
-H "Content-Type: application/json" \
-d '{"data_source": {"type": "inline", "data": [{"x": 1}, {"x": 2}]}, "params": {"my_var": "x"}}'
6.2 工具命名规范
| 项目 | 规范 |
|---|---|
| 文件名 | 小写下划线:t_test_ind.R |
| tool_code | 大写下划线:ST_T_TEST_IND |
| 入口函数 | 固定名称:run_analysis |
6.3 结果格式规范
return(list(
status = "success" | "error" | "blocked",
message = "...",
warnings = c("...") | NULL,
results = list(
# 统计结果(使用 jsonlite::unbox() 保证单值不被包装成数组)
),
report_blocks = list(
# Block-based 标准化输出(Phase E+ 协议),前端 DynamicReport.tsx 统一渲染
# 支持 4 种 Block 类型:markdown / table / image / key_value
# 通过 utils/block_helpers.R 的辅助函数构建
),
plots = list(
"data:image/png;base64,..."
),
trace_log = c("..."),
reproducible_code = "..."
))
6.4 Block-based 输出协议(Phase E+ 标准)
所有工具必须通过 utils/block_helpers.R 构建 report_blocks[],前端 DynamicReport.tsx 根据 block.type 统一渲染。
| 辅助函数 | Block 类型 | 用途 |
|---|---|---|
make_markdown_block(content, title) |
markdown |
文本结论、方法说明 |
make_table_block(headers, rows, title, footnote, metadata) |
table |
统计结果表、系数表、事后比较表 |
make_table_block_from_df(df, title, footnote, digits) |
table |
从 data.frame 快速构建表格 |
make_image_block(base64_data, title, alt) |
image |
图表(base64 编码 PNG) |
make_kv_block(items, title) |
key_value |
检验统计量、模型拟合指标 |
示例:
blocks <- list()
blocks[[length(blocks) + 1]] <- make_kv_block(
list("检验方法" = "Welch t-test", "统计量" = "t = -2.35", "P 值" = "p = .021"),
title = "检验结果"
)
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "组间比较箱线图")
blocks[[length(blocks) + 1]] <- make_markdown_block("两组差异具有统计学意义...", title = "结论")
6.5 各工具参数快速参考
调用
POST /api/v1/skills/{tool_code}时,params对象需要的字段速查。
| tool_code | 必需参数 | 可选参数 |
|---|---|---|
ST_T_TEST_IND |
group_var, value_var |
guardrails.check_normality |
ST_MANN_WHITNEY |
group_var, value_var |
— |
ST_T_TEST_PAIRED |
before_var, after_var |
guardrails.check_normality |
ST_CHI_SQUARE |
var1, var2 |
— |
ST_CORRELATION |
var_x, var_y |
method ("auto" / "pearson" / "spearman") |
ST_LOGISTIC_BINARY |
outcome_var, predictors (数组) |
— |
ST_DESCRIPTIVE |
variables (数组) |
group_var |
ST_FISHER |
var1, var2 |
— |
ST_ANOVA_ONE |
group_var, value_var |
guardrails.check_normality |
ST_WILCOXON |
before_var, after_var |
— |
ST_LINEAR_REG |
outcome_var, predictors (数组) |
confounders (数组) |
ST_BASELINE_TABLE |
group_var |
analyze_vars (数组,不传则自动全选) |
ST_META_ANALYSIS |
data_type (hr / dichotomous / continuous) |
model (random / fixed)、effect_measure (二分类: OR / RR / RD) |
6.6 R 语言陷阱速查(从实际 Bug 中总结)
Phase Deploy 开发中实际踩过的坑,后续开发者必读。每条附真实错误信息和修复方法。
陷阱 1:JSON 数组参数在 R 中是 list,不是 character 向量
错误信息: invalid subscript type 'list'
原因: plumber 解析 JSON ["age", "sex", "bmi"] 后,R 拿到的是 list("age", "sex", "bmi"),不是 c("age", "sex", "bmi")。对 list 做 %in%、[ 等操作都会报错。
# ❌ 错误:直接使用 JSON 传入的数组参数
analyze_vars <- p$analyze_vars
missing <- analyze_vars[!(analyze_vars %in% names(df))] # 报错!
# ✅ 正确:先转换为字符向量
analyze_vars <- as.character(unlist(p$analyze_vars))
missing <- analyze_vars[!(analyze_vars %in% names(df))] # 正常
影响范围: 所有接收数组参数的工具(predictors、variables、analyze_vars、confounders)。
陷阱 2:list() 中不能用表达式做键名
错误信息: unexpected '='
原因: R 的 list() 构造器只接受字面量作为名称,不接受 paste0()、glue() 等函数调用。
# ❌ 错误:用表达式做键名
items <- list(
paste0(var_name, " Median") = "5.2" # 语法错误!
)
# ✅ 正确:先创建 list 再用 [[ 赋值
items <- list()
items[[paste0(var_name, " Median")]] <- "5.2"
陷阱 3:tryCatch 会吞掉 warning 导致结果丢失
错误信息: 无明确错误,但返回 NULL 或非预期结果
原因: tryCatch(expr, warning = function(w) {...}) 捕获第一个 warning 后中断 expr 执行,返回 warning handler 的返回值。gtsummary、car 等包常发 warning,导致主计算被中断。
# ❌ 错误:tryCatch 捕获 warning 会中断执行
tbl <- tryCatch({
tbl_summary(df) %>% add_p() # 如果 add_p() 发 warning,tbl 变成 NULL
}, warning = function(w) {
invokeRestart("muffleWarning") # 在 tryCatch 中无效!
})
# ✅ 正确:withCallingHandlers 处理 warning(不中断执行),tryCatch 只捕获 error
tbl <- tryCatch(
withCallingHandlers(
{ tbl_summary(df) %>% add_p() },
warning = function(w) {
warnings_list <<- c(warnings_list, w$message)
invokeRestart("muffleWarning")
}
),
error = function(e) { return(NULL) }
)
陷阱 4:gtsummary table_body 的 p.value 是 list 列
错误信息: invalid subscript type 'list'
原因: gtsummary 的内部数据结构 tbl$table_body$p.value 是 list 列(每个元素可能是 NULL 或 numeric),不能直接用 < 比较。
# ❌ 错误:直接对 list 列做比较
p_rows <- body[body$p.value < 0.05, ] # 报错!
# ✅ 正确:先 unlist + as.numeric
p_vals <- as.numeric(unlist(body$p.value))
sig_idx <- which(!is.na(p_vals) & p_vals < 0.05)
陷阱 5:浮点数比较不能用 ==
错误信息: 无明确错误,但条件判断逻辑错误
# ❌ 错误:直接比较浮点数
if (sd(values) == 0) { ... } # 可能因精度问题漏判
# ✅ 正确:使用容差比较
if (isTRUE(sd(values) < .Machine$double.eps^0.5)) { ... }
陷阱 6:变量可能为 NULL 导致 glue/round 崩溃
错误信息: non-numeric argument to mathematical function 或 subscript out of bounds
原因: 某些统计结果字段(如 fstatistic)在边界条件下为 NULL。
# ❌ 错误:直接使用可能为 NULL 的值
log_add(glue("F = {round(f_stat[1], 2)}")) # f_stat 为 NULL 时崩溃
# ✅ 正确:先检查再使用
if (!is.null(f_stat)) {
log_add(glue("F = {round(f_stat[1], 2)}"))
} else {
log_add("F = NA")
}
陷阱 7:新增 R 包后 utils/ 修改需要重启容器
现象: make_table_block() 新增了 metadata 参数,但调用时报 unused argument
原因: utils/*.R 在服务启动时一次性加载,不像 tools/*.R 有热重载。修改后必须:
cd r-statistics-service
docker-compose restart
陷阱 8:meta 包返回对数尺度效应量(v1.4 新增)
现象: Meta 分析返回 HR = -0.52,但期望 HR ≈ 0.59
原因: meta R 包的 metagen()/metabin() 对比值类效应量(HR、OR、RR)在对数尺度上计算。TE.random/TE.fixed 返回的是 log(HR) 或 log(OR),而非原始比值。
# ❌ 错误:直接返回 TE(对数尺度)
pooled_effect = ma_result$TE.random # 返回 -0.52(这是 log(HR))
# ✅ 正确:对比值类指标做 exp() 反变换
is_ratio <- sm_label %in% c("HR", "OR", "RR")
display_te <- if (is_ratio) exp(pooled_te) else pooled_te # 返回 0.59(真实 HR)
影响范围: 仅 meta_analysis.R,不影响其他统计工具。连续型指标(MD)无需反变换。
6.7 开发环境新增 R 包
当新工具依赖尚未安装的 R 包时,有两种方式:
方式 1:临时安装到运行中的容器(开发测试用)
# 容器以 appuser 运行,无写权限,需用 root
docker exec -u root ssa-r-statistics R -e "install.packages('新包名', repos='https://cloud.r-project.org/', quiet=TRUE)"
注意:容器重启后丢失,仅用于开发验证。
方式 2:更新 Dockerfile 并重建镜像(正式方案)
- 在
Dockerfile的install.packages()中添加新包名 - 重建:
docker-compose up -d --build
7. 运维指南
7.1 日志查看
# 实时日志
docker logs -f ssa-r-statistics
# 最近 100 行
docker logs --tail 100 ssa-r-statistics
7.2 性能监控
# 容器资源使用
docker stats ssa-r-statistics
7.3 重启服务
# 开发环境
docker-compose restart
# 生产环境 (SAE)
通过 SAE 控制台重启实例
7.4 镜像更新
# 1. 构建新镜像
docker build -t ssa-r-statistics:1.0.2 .
# 2. 推送到镜像仓库
docker tag ssa-r-statistics:1.0.2 registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
docker push registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
# 3. 更新 SAE 部署
8. 常见问题
Q1: 构建时 httpuv 安装失败
错误: fatal error: zlib.h: No such file or directory
解决: 添加 zlib1g-dev 到系统依赖
Q2: 构建时 nloptr 安装失败
错误: CMAKE NOT FOUND
解决: 添加 cmake 到系统依赖
Q3: /tmp 权限问题
错误: cannot open file '/tmp/Rtmpxxx': No such file or directory
解决: 不要在启动命令中清理 /tmp
Q4: DEV_MODE 热重载不生效
原因: 没有挂载 volumes
解决:
volumes:
- ./tools:/app/tools
Q5: 容器启动后无法访问
检查:
- 端口映射是否正确
- 健康检查是否通过
- 查看容器日志
Q6: 数据加载失败(inline 模式)
错误: 内部错误: 数据加载失败
原因: 数据格式不正确,或数据为空
解决:
- 确保
data_source.data是有效的 JSON 数组 - 行格式:
[{"col1": val1}, {"col1": val2}] - 检查是否有空数据或全 NA 列
Q7: R 脚本语法错误
错误: unexpected symbol 或 lexical error
常见原因:
glue()字符串中使用\'转义(应直接使用')- 中文注释编码问题
- 代码块中的花括号不匹配
解决:
# 错误:glue 中的转义
glue("# Cramer\'s V = ...") # ❌
# 正确:直接使用单引号或避免
glue("# Cramer V = ...") # ✅
Q8: JSON 序列化失败
错误: No method asJSON S3 class: table
原因: R 的 table 对象无法直接序列化为 JSON
解决:
# 错误
observed = as.matrix(contingency_table) # ❌ 可能保留 table 属性
# 正确:显式转换为纯数值矩阵
observed = matrix(
as.numeric(contingency_table),
nrow = nrow(contingency_table),
ncol = ncol(contingency_table)
) # ✅
Q9: 新端点返回 404
原因: 修改 plumber.R 后未重启服务
解决:
# 修改 plumber.R 后必须重启
docker-compose restart
# 如果修改了 docker-compose.yml(如添加新 volume)
docker-compose up -d --force-recreate
Q10: 变量类型判断错误(missing value where TRUE/FALSE needed)
原因: 对包含 NA 的数据进行布尔比较
解决:
# 错误
if (var_type == "numeric") { ... } # var_type 可能是 NA
# 正确
if (identical(var_type, "numeric")) { ... } # ✅ 处理 NA
Q11: 修改 utils/ 后新参数报 unused argument
原因: utils/*.R(如 block_helpers.R)在服务启动时加载进内存,不支持热重载(与 tools/*.R 不同)。
解决:
docker-compose restart
Q12: Docker 已 build 但包仍不存在(there is no package called 'xxx')
原因: docker-compose.yml 中的 volumes 挂载会覆盖镜像中的文件,但不影响已安装的 R 包。常见场景是更新了 Dockerfile 却只用了 docker-compose up -d 而没有加 --build。
解决:
# 确保 rebuild
docker-compose up -d --build
# 或临时装包(开发验证)
docker exec -u root ssa-r-statistics R -e "install.packages('xxx', repos='https://cloud.r-project.org/', quiet=TRUE)"
Q13: 工具返回成功但 report_blocks 为空
原因: 返回结构中没有 report_blocks 字段或 blocks 列表为空。
检查清单:
- 确认使用了
utils/block_helpers.R的辅助函数构建 blocks - 确认 return 中包含
report_blocks = blocks - 确认每个 block 至少包含
type字段 - 用测试脚本验证:
node r-statistics-service/tests/run_all_tools_test.js
9. 测试指南
9.1 单工具测试
# 测试 T 检验
curl -s -X POST "http://localhost:8082/api/v1/skills/ST_T_TEST_IND" \
-H "Content-Type: application/json" \
-d '{
"data_source": {
"type": "inline",
"data": [
{"group": "A", "value": 23}, {"group": "A", "value": 25},
{"group": "B", "value": 30}, {"group": "B", "value": 32}
]
},
"params": {"group_var": "group", "value_var": "value"}
}'
9.2 健康检查
curl -s http://localhost:8082/health | jq
9.3 R 工具集中测试脚本(13 工具 + JIT)
项目提供了 R 统计引擎的全工具测试脚本:
# 仅测试 R 服务层(12 工具 + JIT 护栏 + report_blocks 校验)
node r-statistics-service/tests/run_all_tools_test.js
测试覆盖:
- 13 个统计工具(Phase 2A × 7 + Phase Deploy × 5 + ASL × 1)
- JIT 护栏检查(ST_T_TEST_IND / ST_ANOVA_ONE / ST_FISHER / ST_LINEAR_REG)
report_blocks协议校验(类型、必填字段、metadata)
9.4 端到端测试脚本(三层联调)
三层联调测试覆盖 R → Python → Node.js:
cd docs/03-业务模块/SSA-智能统计分析/05-测试文档
node run_e2e_test.js
测试覆盖:
- Layer 1: R 服务(13 个统计工具 + JIT 护栏)
- Layer 2: Python DataProfile API
- Layer 3: Node.js 后端 API(登录 → 会话 → 规划 → 执行)
9.5 Meta 分析引擎专项 E2E 测试(v1.4 新增)
覆盖 Meta 分析的完整链路(Node.js → R Docker → 森林图/漏斗图生成):
cd backend
npx tsx src/modules/asl/meta-analysis/__tests__/meta-e2e-test.ts
测试覆盖(8 个测试项,36 个断言):
- R 服务健康检查 + meta_analysis 工具注册验证
- HR 风险比 Meta 分析(5 个研究,随机效应)
- 二分类 OR Meta 分析(4 个研究)
- 二分类 RR Meta 分析(效应指标切换)
- 连续型 MD Meta 分析(5 个研究)
- 固定效应模型切换
- 边界条件:仅 1 个研究(应返回错误)
- 森林图 + 漏斗图 Base64 PNG 生成验证
最近一次测试结果(2026-02-26):
Results: 36 passed, 0 failed (1.3s)
HR = 0.5948 [0.5255, 0.6733], I² = 15.4%
OR = 0.4530 [0.3328, 0.6166], p < .001
MD = -1.4923 [-1.9098, -1.0749], I² = 70%
附录:文件结构
r-statistics-service/
├── Dockerfile # 生产镜像定义(含 gtsummary/gt/broom/scales/gridExtra/meta)
├── docker-compose.yml # 开发环境编排(含 volume 挂载)
├── renv.lock # R 包版本锁定(备用)
├── .Rprofile # R 启动配置(备用)
├── plumber.R # API 入口(含 JIT 护栏端点,自动发现 tools/ 目录)
├── utils/
│ ├── data_loader.R # 数据加载(支持行格式/列格式)
│ ├── guardrails.R # 统计护栏 + JIT 检查(12 工具全覆盖)
│ ├── error_codes.R # 错误映射
│ ├── result_formatter.R # 结果格式化
│ └── block_helpers.R # Block-based 输出辅助函数(Phase E+ 协议)
├── tools/ # 统计工具(13 个)
│ ├── t_test_ind.R # 独立样本 T 检验
│ ├── t_test_paired.R # 配对 T 检验
│ ├── mann_whitney.R # Mann-Whitney U 检验
│ ├── chi_square.R # 卡方检验
│ ├── correlation.R # 相关分析
│ ├── logistic_binary.R # 二元 Logistic 回归
│ ├── descriptive.R # 描述性统计
│ ├── fisher.R # 🆕 Fisher 精确检验(Phase Deploy)
│ ├── anova_one.R # 🆕 单因素方差分析(Phase Deploy)
│ ├── wilcoxon.R # 🆕 Wilcoxon 符号秩检验(Phase Deploy)
│ ├── linear_reg.R # 🆕 线性回归(Phase Deploy)
│ ├── baseline_table.R # 🆕 基线特征表 — 复合工具(Phase Deploy)
│ └── meta_analysis.R # 🆕 Meta 分析引擎 — HR/二分类/连续型(ASL 工具 5)
├── tests/
│ ├── run_all_tools_test.js # 🆕 全工具自动化测试(12 工具 + JIT + blocks 校验)
│ ├── test_t_test.json # T 检验测试数据
│ ├── test_fisher.json # Fisher 测试数据
│ ├── test_anova_one.json # ANOVA 测试数据
│ ├── test_wilcoxon.json # Wilcoxon 测试数据
│ ├── test_linear_reg.json # 线性回归测试数据
│ ├── test_baseline_table.json # 基线表测试数据
│ └── fixtures/
│ └── normal_data.csv # 测试数据
├── metadata/ # 工具元数据(预留)
└── templates/ # 解释模板(预留)
更新日志
| 版本 | 日期 | 更新内容 |
|---|---|---|
| v1.4 | 2026-02-26 | ASL Meta 分析引擎:工具 12→13(+ST_META_ANALYSIS),Dockerfile 新增 meta 包,新增 §5.6 Meta 分析 API 示例、陷阱 8(对数尺度反变换)、§9.5 Meta E2E 测试(36 断言全通过),架构图更新 ASL 调用方 |
| v1.3 | 2026-02-22 | 开发者体验增强:新工具模板补全 report_blocks(§6.1)、各工具 params 速查表(§6.5)、R 语言 7 大陷阱实录(§6.6)、新增 R 包操作指南(§6.7)、新增 Q11-Q13 常见问题 |
| v1.2 | 2026-02-22 | Phase Deploy 完成:工具 7→12(+Fisher/ANOVA/Wilcoxon/线性回归/基线表)、Dockerfile 新增 gtsummary 等 5 包、Block-based 输出协议文档化(§6.4)、全工具测试脚本 |
| v1.1 | 2026-02-20 | Phase 2A 完成:7 个统计工具、JIT 护栏、热重载说明、常见问题补充 |
| v1.0 | 2026-02-19 | 初始版本:架构设计、部署指南、T 检验工具 |
文档结束