feat(ssa): Complete QPER architecture - Query, Planner, Execute, Reflection layers
Implement the full QPER intelligent analysis pipeline: - Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement - Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging - Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output - Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification End-to-end test: 40/40 passed across two complete analysis scenarios. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ source("utils/error_codes.R")
|
||||
source("utils/data_loader.R")
|
||||
source("utils/guardrails.R")
|
||||
source("utils/result_formatter.R")
|
||||
source("utils/block_helpers.R")
|
||||
|
||||
# 工具目录
|
||||
tools_dir <- "tools"
|
||||
|
||||
@@ -213,6 +213,60 @@ cat("Cramer V =", round(cramers_v, 3), "\\n")
|
||||
mosaicplot(contingency_table, main = "Mosaic Plot", color = TRUE)
|
||||
')
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
# Block 1: 列联表
|
||||
table_headers <- c(var1, as.character(colnames(contingency_table)))
|
||||
table_rows <- lapply(seq_len(nrow(contingency_table)), function(i) {
|
||||
c(as.character(rownames(contingency_table)[i]), as.character(contingency_table[i, ]))
|
||||
})
|
||||
blocks <- list(
|
||||
make_table_block(table_headers, table_rows, title = "列联表")
|
||||
)
|
||||
|
||||
# Block 2: 检验结果键值对
|
||||
if (use_fisher) {
|
||||
kv_items <- list(
|
||||
"方法" = method_used,
|
||||
"P 值" = output_results$p_value_fmt
|
||||
)
|
||||
if (!is.null(output_results$odds_ratio)) {
|
||||
kv_items[["比值比"]] <- as.character(round(as.numeric(output_results$odds_ratio), 4))
|
||||
}
|
||||
if (!is.null(output_results$conf_int)) {
|
||||
kv_items[["95% 置信区间"]] <- sprintf("[%.4f, %.4f]", output_results$conf_int[1], output_results$conf_int[2])
|
||||
}
|
||||
} else {
|
||||
kv_items <- list(
|
||||
"方法" = method_used,
|
||||
"χ² 统计量" = as.character(round(as.numeric(output_results$statistic), 4)),
|
||||
"自由度" = as.character(output_results$df),
|
||||
"P 值" = output_results$p_value_fmt,
|
||||
"Cramér's V" = as.character(output_results$effect_size$cramers_v),
|
||||
"效应量解释" = output_results$effect_size$interpretation
|
||||
)
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(kv_items, title = "检验结果")
|
||||
|
||||
# Block 3: 马赛克图(若有)
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "马赛克图")
|
||||
}
|
||||
|
||||
# Block 4: 结论摘要
|
||||
p_val <- as.numeric(output_results$p_value)
|
||||
conclusion <- if (p_val < 0.05) {
|
||||
glue("在 α=0.05 水平下,{var1} 与 {var2} 之间存在显著关联(P {output_results$p_value_fmt})。")
|
||||
} else {
|
||||
glue("在 α=0.05 水平下,未发现 {var1} 与 {var2} 之间的显著关联(P {output_results$p_value_fmt})。")
|
||||
}
|
||||
if (!use_fisher) {
|
||||
conclusion <- paste0(conclusion, " 效应量为", output_results$effect_size$interpretation,
|
||||
"(Cramér's V = ", output_results$effect_size$cramers_v, ")。")
|
||||
} else if (!is.null(output_results$odds_ratio)) {
|
||||
conclusion <- paste0(conclusion, " 比值比 = ", round(as.numeric(output_results$odds_ratio), 4), "。")
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(conclusion, title = "结论摘要")
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
@@ -221,6 +275,7 @@ mosaicplot(contingency_table, main = "Mosaic Plot", color = TRUE)
|
||||
message = "分析完成",
|
||||
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
|
||||
results = output_results,
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -195,12 +195,50 @@ ggplot(df, aes(x = .data[[var_x]], y = .data[[var_y]])) +
|
||||
if (final_method == "pearson" && !is.null(result$conf.int)) {
|
||||
output_results$conf_int <- as.numeric(result$conf.int)
|
||||
}
|
||||
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 分析概况
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(list(
|
||||
"变量 X" = var_x,
|
||||
"变量 Y" = var_y,
|
||||
"样本量" = as.character(n),
|
||||
"分析方法" = final_method
|
||||
), title = "分析概况")
|
||||
|
||||
# Block 2: 相关分析结果表
|
||||
ci_str <- if (final_method == "pearson" && !is.null(result$conf.int)) {
|
||||
sprintf("[%.3f, %.3f]", result$conf.int[1], result$conf.int[2])
|
||||
} else {
|
||||
"-"
|
||||
}
|
||||
corr_headers <- c("r 值", "P 值", "95% CI", "相关强度")
|
||||
corr_rows <- list(c(
|
||||
as.character(round(r_value, 4)),
|
||||
format_p_value(p_value),
|
||||
ci_str,
|
||||
r_interpretation
|
||||
))
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(corr_headers, corr_rows, title = "相关分析结果")
|
||||
|
||||
# Block 3: 散点图
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "散点图", alt = paste(var_x, "vs", var_y))
|
||||
}
|
||||
|
||||
# Block 4: 结论摘要
|
||||
conclusion_text <- glue(
|
||||
"**{var_x}** 与 **{var_y}** 的 {final_method} 相关系数为 r = {round(r_value, 3)} (P {format_p_value(p_value)}),相关强度为 **{r_interpretation}**。"
|
||||
)
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(conclusion_text, title = "结论摘要")
|
||||
|
||||
return(list(
|
||||
status = "success",
|
||||
message = "分析完成",
|
||||
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
|
||||
results = output_results,
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -199,7 +199,83 @@ if (any(categorical_vars)) {{
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 数据概况
|
||||
kv_items <- list(
|
||||
"总样本量" = as.character(summary_stats$n_total),
|
||||
"变量数" = as.character(summary_stats$n_variables),
|
||||
"数值变量数" = as.character(summary_stats$n_numeric),
|
||||
"分类变量数" = as.character(summary_stats$n_categorical)
|
||||
)
|
||||
if (!is.null(groups)) {
|
||||
kv_items$group_var <- group_var
|
||||
kv_items$groups <- paste(sapply(summary_stats$groups, function(g) paste0(g$name, "(n=", g$n, ")")), collapse = ", ")
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(kv_items, title = "数据概况")
|
||||
|
||||
# Block 2: 数值变量汇总表
|
||||
numeric_vars <- names(results_list)[sapply(results_list, function(x) {
|
||||
if (is.list(x) && !is.null(x$type)) x$type == "numeric" else FALSE
|
||||
})]
|
||||
if (length(numeric_vars) > 0) {
|
||||
if (is.null(groups)) {
|
||||
num_headers <- c("变量名", "n", "mean", "sd", "median", "Q1", "Q3", "min", "max")
|
||||
num_rows <- lapply(numeric_vars, function(v) {
|
||||
s <- results_list[[v]]
|
||||
c(v, as.character(s$n), as.character(s$mean), as.character(s$sd),
|
||||
as.character(s$median), as.character(s$q1), as.character(s$q3),
|
||||
as.character(s$min), as.character(s$max))
|
||||
})
|
||||
} else {
|
||||
num_headers <- c("变量名", as.character(groups))
|
||||
num_rows <- lapply(numeric_vars, function(v) {
|
||||
s <- results_list[[v]]
|
||||
row <- c(v)
|
||||
for (g in groups) {
|
||||
gs <- s$by_group[[as.character(g)]]
|
||||
row <- c(row, if (!is.null(gs$formatted)) gs$formatted else "-")
|
||||
}
|
||||
row
|
||||
})
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(num_headers, num_rows, title = "数值变量汇总表")
|
||||
}
|
||||
|
||||
# Block 3: 分类变量汇总表
|
||||
cat_vars <- names(results_list)[sapply(results_list, function(x) {
|
||||
if (is.list(x) && !is.null(x$type)) x$type == "categorical" else FALSE
|
||||
})]
|
||||
if (length(cat_vars) > 0) {
|
||||
cat_headers <- c("变量名", "水平", "n", "百分比")
|
||||
cat_rows <- list()
|
||||
for (v in cat_vars) {
|
||||
s <- results_list[[v]]
|
||||
if (is.null(groups)) {
|
||||
for (lev in s$levels) {
|
||||
cat_rows[[length(cat_rows) + 1]] <- c(v, lev$level, as.character(lev$n), paste0(lev$pct, "%"))
|
||||
}
|
||||
} else {
|
||||
for (g in groups) {
|
||||
gs <- s$by_group[[as.character(g)]]
|
||||
for (lev in gs$levels) {
|
||||
cat_rows[[length(cat_rows) + 1]] <- c(paste0(v, " (", g, ")"), lev$level, as.character(lev$n), paste0(lev$pct, "%"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (length(cat_rows) > 0) {
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(cat_headers, cat_rows, title = "分类变量汇总表")
|
||||
}
|
||||
}
|
||||
|
||||
# Block 4+: 各图表
|
||||
for (i in seq_along(plots)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(plots[[i]], title = paste0("图表 ", i), alt = paste0("描述性统计图 ", i))
|
||||
}
|
||||
|
||||
return(list(
|
||||
status = "success",
|
||||
message = "分析完成",
|
||||
@@ -208,6 +284,7 @@ if (any(categorical_vars)) {{
|
||||
summary = summary_stats,
|
||||
variables = results_list
|
||||
),
|
||||
report_blocks = blocks,
|
||||
plots = plots,
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -254,7 +254,59 @@ cat("AIC:", AIC(model), "\\n")
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 模型概况
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(list(
|
||||
"模型公式" = formula_str,
|
||||
"观测数" = as.character(nrow(df)),
|
||||
"预测变量数" = as.character(n_predictors),
|
||||
"AIC" = as.character(round(aic, 2)),
|
||||
"Nagelkerke R²" = as.character(round(r2_nagelkerke, 4)),
|
||||
"EPV" = as.character(round(epv, 1))
|
||||
), title = "模型概况")
|
||||
|
||||
# Block 2: 回归系数表
|
||||
coef_headers <- c("变量", "OR", "95% CI", "P 值", "显著性")
|
||||
coef_rows <- lapply(coefficients_list, function(row) {
|
||||
ci_str <- sprintf("[%.3f, %.3f]", row$ci_lower, row$ci_upper)
|
||||
sig <- if (row$significant) "*" else ""
|
||||
c(row$variable, as.character(row$OR), ci_str, row$p_value_fmt, sig)
|
||||
})
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(coef_headers, coef_rows, title = "回归系数表", footnote = "* P < 0.05")
|
||||
|
||||
# Block 3: VIF 表(如存在)
|
||||
if (!is.null(vif_results) && length(vif_results) > 0) {
|
||||
vif_headers <- c("变量", "VIF")
|
||||
vif_rows <- lapply(vif_results, function(row) c(row$variable, as.character(row$vif)))
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(vif_headers, vif_rows, title = "方差膨胀因子 (VIF)")
|
||||
}
|
||||
|
||||
# Block 4: 森林图(如存在)
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "森林图", alt = "Odds Ratios Forest Plot")
|
||||
}
|
||||
|
||||
# Block 5: 结论摘要
|
||||
sig_vars <- sapply(coefficients_list, function(r) if (r$variable != "(Intercept)" && r$significant) r$variable else NULL)
|
||||
sig_vars <- unlist(sig_vars[!sapply(sig_vars, is.null)])
|
||||
conclusion_lines <- c(
|
||||
glue("模型拟合指标:AIC = {round(aic, 2)},Nagelkerke R² = {round(r2_nagelkerke, 4)}。"),
|
||||
""
|
||||
)
|
||||
if (length(sig_vars) > 0) {
|
||||
conclusion_lines <- c(conclusion_lines,
|
||||
glue("在 α = 0.05 水平下,以下变量具有统计学意义:**{paste(sig_vars, collapse = '**, **')}**。"),
|
||||
""
|
||||
)
|
||||
} else {
|
||||
conclusion_lines <- c(conclusion_lines, "在 α = 0.05 水平下,无预测变量达到统计学意义。", "")
|
||||
}
|
||||
conclusion_lines <- c(conclusion_lines, glue("EPV = {round(epv, 1)}(建议 ≥ 10)。"))
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(paste(conclusion_lines, collapse = "\n"), title = "结论摘要")
|
||||
|
||||
return(list(
|
||||
status = "success",
|
||||
message = "分析完成",
|
||||
@@ -274,6 +326,7 @@ cat("AIC:", AIC(model), "\\n")
|
||||
vif = vif_results,
|
||||
epv = jsonlite::unbox(round(epv, 1))
|
||||
),
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -173,6 +173,62 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) +
|
||||
labs(title = paste("Distribution of", value_var, "by", group_var))
|
||||
')
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
log_add("构建 report_blocks")
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 样本概况(两组 n, median, IQR)
|
||||
g1_label <- as.character(groups[1])
|
||||
g2_label <- as.character(groups[2])
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(
|
||||
title = "样本概况",
|
||||
items = list(
|
||||
list(key = paste0(g1_label, " (n, Median, IQR)"),
|
||||
value = paste0("n=", n1, ", ", round(median(g1_vals), 3), ", ", round(IQR(g1_vals), 3))),
|
||||
list(key = paste0(g2_label, " (n, Median, IQR)"),
|
||||
value = paste0("n=", n2, ", ", round(median(g2_vals), 3), ", ", round(IQR(g2_vals), 3)))
|
||||
)
|
||||
)
|
||||
|
||||
# Block 2: 检验结果(U 统计量, Z 值, P 值, 效应量 r)
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(
|
||||
title = "Mann-Whitney U 检验结果",
|
||||
headers = c("U 统计量", "Z 值", "P 值", "效应量 r", "效应量解释"),
|
||||
rows = list(
|
||||
list(
|
||||
round(as.numeric(U), 4),
|
||||
round(z_value, 4),
|
||||
format_p_value(result$p.value),
|
||||
round(effect_r, 4),
|
||||
effect_interpretation
|
||||
)
|
||||
),
|
||||
footnote = "Wilcoxon rank sum test with continuity correction"
|
||||
)
|
||||
|
||||
# Block 3: 箱线图(如果 plot_base64 不为 NULL)
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(
|
||||
base64_data = plot_base64,
|
||||
title = paste0(value_var, " by ", group_var),
|
||||
alt = paste("箱线图:", value_var, "按", group_var, "分组")
|
||||
)
|
||||
}
|
||||
|
||||
# Block 4: 结论摘要
|
||||
sig <- if (result$p.value < 0.05) "存在统计学显著差异" else "差异无统计学意义"
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(
|
||||
title = "结果摘要",
|
||||
content = paste0(
|
||||
"两组 **", value_var, "** 的比较(Mann-Whitney U 检验):",
|
||||
"U = ", round(as.numeric(U), 2),
|
||||
",Z = ", round(z_value, 3),
|
||||
",P ", format_p_value(result$p.value),
|
||||
",效应量 r = ", round(effect_r, 3), "(", effect_interpretation, ")。",
|
||||
"两组间", sig, "。"
|
||||
)
|
||||
)
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
@@ -209,6 +265,7 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) +
|
||||
)
|
||||
)
|
||||
),
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -180,6 +180,66 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) +
|
||||
labs(title = paste("Distribution of", value_var, "by", group_var))
|
||||
')
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
log_add("构建 report_blocks")
|
||||
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 描述统计键值对
|
||||
g1_label <- as.character(groups[1])
|
||||
g2_label <- as.character(groups[2])
|
||||
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(
|
||||
title = "样本概况",
|
||||
items = list(
|
||||
list(key = paste0(group_var, " = ", g1_label, " (n)"), value = as.character(length(g1_vals))),
|
||||
list(key = paste0(group_var, " = ", g2_label, " (n)"), value = as.character(length(g2_vals))),
|
||||
list(key = paste0(g1_label, " Mean ± SD"),
|
||||
value = paste0(round(mean(g1_vals), 3), " \u00b1 ", round(sd(g1_vals), 3))),
|
||||
list(key = paste0(g2_label, " Mean ± SD"),
|
||||
value = paste0(round(mean(g2_vals), 3), " \u00b1 ", round(sd(g2_vals), 3)))
|
||||
)
|
||||
)
|
||||
|
||||
# Block 2: 检验结果表格
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(
|
||||
title = "独立样本 T 检验结果",
|
||||
headers = c("统计量", "自由度", "P 值", "95% CI 下限", "95% CI 上限", "均值差"),
|
||||
rows = list(
|
||||
list(
|
||||
round(as.numeric(result$statistic), 4),
|
||||
round(as.numeric(result$parameter), 2),
|
||||
format_p_value(result$p.value),
|
||||
round(result$conf.int[1], 4),
|
||||
round(result$conf.int[2], 4),
|
||||
round(diff(result$estimate), 4)
|
||||
)
|
||||
),
|
||||
footnote = result$method
|
||||
)
|
||||
|
||||
# Block 3: 箱线图
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(
|
||||
base64_data = plot_base64,
|
||||
title = paste0(value_var, " by ", group_var),
|
||||
alt = paste("箱线图:", value_var, "按", group_var, "分组")
|
||||
)
|
||||
}
|
||||
|
||||
# Block 4: 结论摘要
|
||||
sig <- if (result$p.value < 0.05) "存在统计学显著差异" else "差异无统计学意义"
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(
|
||||
title = "结果摘要",
|
||||
content = paste0(
|
||||
"两组 **", value_var, "** 的比较(", result$method, "):",
|
||||
"t = ", round(as.numeric(result$statistic), 3),
|
||||
",df = ", round(as.numeric(result$parameter), 1),
|
||||
",P ", format_p_value(result$p.value),
|
||||
"。两组间", sig, "。"
|
||||
)
|
||||
)
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
@@ -196,10 +256,11 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) +
|
||||
conf_int = as.numeric(result$conf.int),
|
||||
estimate = as.numeric(result$estimate),
|
||||
group_stats = list(
|
||||
list(group = as.character(groups[1]), n = length(g1_vals), mean = mean(g1_vals), sd = sd(g1_vals)),
|
||||
list(group = as.character(groups[2]), n = length(g2_vals), mean = mean(g2_vals), sd = sd(g2_vals))
|
||||
list(group = g1_label, n = length(g1_vals), mean = mean(g1_vals), sd = sd(g1_vals)),
|
||||
list(group = g2_label, n = length(g2_vals), mean = mean(g2_vals), sd = sd(g2_vals))
|
||||
)
|
||||
),
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
@@ -226,7 +226,98 @@ ggplot(df_long, aes(x = time, y = value, group = id)) +
|
||||
theme_minimal() +
|
||||
labs(title = "Paired Comparison")
|
||||
')
|
||||
|
||||
|
||||
# ===== 构建 report_blocks =====
|
||||
d <- output_results$descriptive
|
||||
blocks <- list()
|
||||
|
||||
# Block 1: 样本概况
|
||||
blocks[[length(blocks) + 1]] <- make_kv_block(
|
||||
title = "样本概况",
|
||||
items = list(
|
||||
list(key = paste0(before_var, " (n)"), value = as.character(d$before$n)),
|
||||
list(key = paste0(before_var, " Mean"), value = as.character(d$before$mean)),
|
||||
list(key = paste0(before_var, " SD"), value = as.character(d$before$sd)),
|
||||
list(key = paste0(before_var, " Median"), value = as.character(d$before$median)),
|
||||
list(key = paste0(after_var, " (n)"), value = as.character(d$after$n)),
|
||||
list(key = paste0(after_var, " Mean"), value = as.character(d$after$mean)),
|
||||
list(key = paste0(after_var, " SD"), value = as.character(d$after$sd)),
|
||||
list(key = paste0(after_var, " Median"), value = as.character(d$after$median)),
|
||||
list(key = "差值 Mean", value = as.character(d$difference$mean)),
|
||||
list(key = "差值 SD", value = as.character(d$difference$sd))
|
||||
)
|
||||
)
|
||||
|
||||
# Block 2: 检验结果表格(根据 use_wilcoxon 区分)
|
||||
if (use_wilcoxon) {
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(
|
||||
title = "Wilcoxon 符号秩检验结果",
|
||||
headers = c("统计量 V", "P 值", "效应量 r", "效应量解释"),
|
||||
rows = list(list(
|
||||
round(as.numeric(output_results$statistic), 4),
|
||||
format_p_value(output_results$p_value),
|
||||
round(output_results$effect_size$r, 4),
|
||||
output_results$effect_size$interpretation
|
||||
)),
|
||||
footnote = method_used
|
||||
)
|
||||
} else {
|
||||
ci_str <- if (length(output_results$conf_int) >= 2) {
|
||||
sprintf("[%.4f, %.4f]", output_results$conf_int[1], output_results$conf_int[2])
|
||||
} else {
|
||||
"—"
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_table_block(
|
||||
title = "配对 T 检验结果",
|
||||
headers = c("t", "df", "P 值", "95% CI", "Cohen's d", "效应量解释"),
|
||||
rows = list(list(
|
||||
round(as.numeric(output_results$statistic), 4),
|
||||
round(as.numeric(output_results$df), 2),
|
||||
format_p_value(output_results$p_value),
|
||||
ci_str,
|
||||
round(output_results$effect_size$cohens_d, 4),
|
||||
output_results$effect_size$interpretation
|
||||
)),
|
||||
footnote = method_used
|
||||
)
|
||||
}
|
||||
|
||||
# Block 3: 配对比较图
|
||||
if (!is.null(plot_base64)) {
|
||||
blocks[[length(blocks) + 1]] <- make_image_block(
|
||||
base64_data = plot_base64,
|
||||
title = paste0("配对比较: ", before_var, " vs ", after_var),
|
||||
alt = paste("配对比较图:", before_var, "与", after_var)
|
||||
)
|
||||
}
|
||||
|
||||
# Block 4: 结论摘要
|
||||
sig <- if (output_results$p_value < 0.05) "存在统计学显著差异" else "差异无统计学意义"
|
||||
if (use_wilcoxon) {
|
||||
concl <- paste0(
|
||||
"配对样本 **", before_var, "** 与 **", after_var, "** 的比较(", method_used, "):",
|
||||
"V = ", round(as.numeric(output_results$statistic), 3),
|
||||
",P ", format_p_value(output_results$p_value),
|
||||
",效应量 r = ", round(output_results$effect_size$r, 3),
|
||||
"(", output_results$effect_size$interpretation, ")。",
|
||||
sig, "。"
|
||||
)
|
||||
} else {
|
||||
concl <- paste0(
|
||||
"配对样本 **", before_var, "** 与 **", after_var, "** 的比较(", method_used, "):",
|
||||
"t = ", round(as.numeric(output_results$statistic), 3),
|
||||
",df = ", round(as.numeric(output_results$df), 1),
|
||||
",P ", format_p_value(output_results$p_value),
|
||||
",Cohen's d = ", round(output_results$effect_size$cohens_d, 3),
|
||||
"(", output_results$effect_size$interpretation, ")。",
|
||||
sig, "。"
|
||||
)
|
||||
}
|
||||
blocks[[length(blocks) + 1]] <- make_markdown_block(
|
||||
title = "结果摘要",
|
||||
content = concl
|
||||
)
|
||||
|
||||
# ===== 返回结果 =====
|
||||
log_add("分析完成")
|
||||
|
||||
@@ -235,6 +326,7 @@ ggplot(df_long, aes(x = time, y = value, group = id)) +
|
||||
message = "分析完成",
|
||||
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
|
||||
results = output_results,
|
||||
report_blocks = blocks,
|
||||
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
|
||||
trace_log = logs,
|
||||
reproducible_code = as.character(reproducible_code)
|
||||
|
||||
85
r-statistics-service/utils/block_helpers.R
Normal file
85
r-statistics-service/utils/block_helpers.R
Normal file
@@ -0,0 +1,85 @@
|
||||
# utils/block_helpers.R
|
||||
# Block-based 输出协议 — 构造函数
|
||||
#
|
||||
# 所有 R 工具通过这些函数构建 report_blocks[],
|
||||
# 前端 DynamicReport.tsx 根据 block.type 统一渲染。
|
||||
# 支持 4 种 Block 类型:markdown / table / image / key_value
|
||||
|
||||
#' 构造 Markdown 文本块
|
||||
#' @param content Markdown 格式文本(支持标题、列表、加粗等)
|
||||
#' @param title 可选标题(前端渲染为区块标题)
|
||||
#' @return block list
|
||||
make_markdown_block <- function(content, title = NULL) {
|
||||
block <- list(type = "markdown", content = content)
|
||||
if (!is.null(title)) block$title <- title
|
||||
block
|
||||
}
|
||||
|
||||
#' 构造表格块
|
||||
#' @param headers 列名字符向量,如 c("组别", "均值", "标准差")
|
||||
#' @param rows 行数据列表,每行为字符向量,如 list(c("A", "5.2", "1.3"), ...)
|
||||
#' @param title 可选表格标题
|
||||
#' @param footnote 可选脚注(如方法说明)
|
||||
#' @return block list
|
||||
make_table_block <- function(headers, rows, title = NULL, footnote = NULL) {
|
||||
block <- list(
|
||||
type = "table",
|
||||
headers = as.list(headers),
|
||||
rows = lapply(rows, as.list)
|
||||
)
|
||||
if (!is.null(title)) block$title <- title
|
||||
if (!is.null(footnote)) block$footnote <- footnote
|
||||
block
|
||||
}
|
||||
|
||||
#' 从 data.frame 构造表格块(便捷方法)
|
||||
#' @param df data.frame
|
||||
#' @param title 可选表格标题
|
||||
#' @param footnote 可选脚注
|
||||
#' @param digits 数值列保留小数位数,默认 3
|
||||
#' @return block list
|
||||
make_table_block_from_df <- function(df, title = NULL, footnote = NULL, digits = 3) {
|
||||
headers <- colnames(df)
|
||||
|
||||
rows <- lapply(seq_len(nrow(df)), function(i) {
|
||||
lapply(df[i, , drop = FALSE], function(val) {
|
||||
if (is.numeric(val)) {
|
||||
format(round(val, digits), nsmall = digits)
|
||||
} else {
|
||||
as.character(val)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
make_table_block(headers, rows, title = title, footnote = footnote)
|
||||
}
|
||||
|
||||
#' 构造图片块
|
||||
#' @param base64_data 完整的 data URI,如 "data:image/png;base64,..."
|
||||
#' @param title 可选图片标题
|
||||
#' @param alt 可选 alt 文本(无障碍 + Word 导出用)
|
||||
#' @return block list
|
||||
make_image_block <- function(base64_data, title = NULL, alt = NULL) {
|
||||
block <- list(type = "image", data = base64_data)
|
||||
if (!is.null(title)) block$title <- title
|
||||
if (!is.null(alt)) block$alt <- alt
|
||||
block
|
||||
}
|
||||
|
||||
#' 构造键值对块
|
||||
#' @param items 命名列表或 list(list(key=..., value=...), ...)
|
||||
#' @param title 可选标题
|
||||
#' @return block list
|
||||
make_kv_block <- function(items, title = NULL) {
|
||||
if (!is.null(names(items)) && length(names(items)) > 0 && names(items)[1] != "") {
|
||||
kv_list <- lapply(names(items), function(k) {
|
||||
list(key = k, value = as.character(items[[k]]))
|
||||
})
|
||||
} else {
|
||||
kv_list <- items
|
||||
}
|
||||
|
||||
block <- list(type = "key_value", items = kv_list)
|
||||
if (!is.null(title)) block$title <- title
|
||||
block
|
||||
}
|
||||
Reference in New Issue
Block a user