# ===================================================== # 芯片数据分析模块 (Microarray Data Analysis) # ===================================================== # 功能: # 1. 解析 GEO Series Matrix 文件 # 2. 解析 SOFT 平台注释文件 # 3. 探针注释 (探针ID → 基因符号) # 4. limma 差异分析 # 5. 与现有下游模块集成 (KEGG/GO/GSEA/TF/通路活性) # ===================================================== # ===================================================== # 1. GEO Series Matrix 文件解析 # ===================================================== #' 解析 GEO Series Matrix 文件 #' #' @param file_path Series Matrix 文件路径 #' @return list 包含表达矩阵和元数据 parse_geo_series_matrix <- function(file_path) { cat("📂 开始解析 GEO Series Matrix 文件...\n") tryCatch({ # 读取所有行 lines <- readLines(file_path, warn = FALSE) # 查找数据起始标记 start_idx <- which(lines == "!series_matrix_table_begin") if (length(start_idx) == 0) { return(list( success = FALSE, error = "未找到 !series_matrix_table_begin 标记,这不是有效的 GEO Series Matrix 文件" )) } start_idx <- start_idx[1] + 1 # 跳过标记行本身 # 查找数据结束标记(可选) end_idx <- which(lines == "!series_matrix_table_end") if (length(end_idx) > 0) { end_idx <- end_idx[1] - 1 } else { # 如果没有结束标记,读取到文件末尾 end_idx <- length(lines) } cat(sprintf("✅ 找到数据区域: 第 %d - %d 行\n", start_idx, end_idx)) # 提取矩阵数据 matrix_lines <- lines[start_idx:end_idx] # 读取为数据框 text_connection <- textConnection(matrix_lines) expr_matrix <- read.table( text_connection, header = TRUE, row.names = 1, sep = "\t", quote = "\"", # 允许引号 comment.char = "", stringsAsFactors = FALSE, check.names = FALSE ) close(text_connection) # 去除列名中的引号 colnames(expr_matrix) <- gsub('"', '', colnames(expr_matrix)) rownames(expr_matrix) <- gsub('"', '', rownames(expr_matrix)) cat(sprintf("✅ 清理引号后样本名示例: %s\n", paste(head(colnames(expr_matrix), 3), collapse = ", "))) cat(sprintf("✅ 清理引号后探针ID示例: %s\n", paste(head(rownames(expr_matrix), 3), collapse = ", "))) # 提取元数据(用于样本分组提示) metadata <- extract_geo_metadata(lines[1:(start_idx-2)]) cat(sprintf("✅ 解析完成: %d 探针 × %d 样本\n", nrow(expr_matrix), ncol(expr_matrix))) return(list( success = TRUE, matrix = expr_matrix, metadata = metadata, n_probes = nrow(expr_matrix), n_samples = ncol(expr_matrix), sample_names = colnames(expr_matrix) )) }, error = function(e) { return(list( success = FALSE, error = paste("解析文件时出错:", e$message) )) }) } #' 提取 GEO 元数据 #' #' @param metadata_lines 元数据行 #' @return list 包含样本描述等信息 extract_geo_metadata <- function(metadata_lines) { result <- list( has_metadata = FALSE, sample_descriptions = NULL, sample_titles = NULL ) # 查找样本描述行 desc_line <- grep("^!Sample_description", metadata_lines, value = TRUE) if (length(desc_line) > 0) { # 提取描述信息 descriptions <- gsub("^!Sample_description\t", "", desc_line) descriptions <- gsub('"', '', descriptions) # 分割成向量 descriptions <- strsplit(descriptions[1], "\t")[[1]] result$sample_descriptions <- descriptions result$has_metadata <- TRUE } # 查找样本标题行 title_line <- grep("^!Sample_title", metadata_lines, value = TRUE) if (length(title_line) > 0) { # 提取标题信息 titles <- gsub("^!Sample_title\t", "", title_line) titles <- gsub('"', '', titles) # 分割成向量 titles <- strsplit(titles[1], "\t")[[1]] result$sample_titles <- titles result$has_metadata <- TRUE } return(result) } # ===================================================== # 2. SOFT 平台文件解析 # ===================================================== #' 解析 SOFT 平台注释文件 #' #' @param file_path SOFT 文件路径 #' @param separator 字段分隔符(正则表达式,默认为Tab) #' @return list 包含原始表格和探针-基因映射 parse_platform_annotation <- function(file_path, separator = "\t") { cat("📋 开始解析 SOFT 平台文件...\n") cat(sprintf("📋 使用分隔符: %s\n", separator)) tryCatch({ lines <- readLines(file_path, warn = FALSE) # 查找平台表起始标记 start_idx <- which(lines == "!platform_table_begin") if (length(start_idx) == 0) { return(list( success = FALSE, error = "未找到 !platform_table_begin 标记" )) } start_idx <- start_idx[1] + 1 # 读取到文件末尾或下一个 ^ 标记 remaining_lines <- lines[start_idx:length(lines)] next_section <- which(grepl("^\\^", remaining_lines)) if (length(next_section) > 0) { end_idx <- start_idx + next_section[1] - 2 } else { end_idx <- length(lines) } table_lines <- lines[start_idx:end_idx] # 读取表头(使用用户指定的分隔符) header <- strsplit(table_lines[1], separator)[[1]] # 读取数据(使用用户指定的分隔符) text_conn <- textConnection(table_lines) raw_table <- read.table( text_conn, header = TRUE, sep = separator, quote = "", stringsAsFactors = FALSE, fill = TRUE, check.names = FALSE ) close(text_conn) cat(sprintf("📋 平台注释文件解析: %d 列 × %d 行\n", ncol(raw_table), nrow(raw_table))) cat("📋 列名:", paste(colnames(raw_table), collapse = ", "), "\n") # 🔍 显示各列的示例数据,帮助用户选择 cat("📋 各列示例数据:\n") for (col in colnames(raw_table)) { sample_vals <- head(raw_table[[col]][!is.na(raw_table[[col]]) & raw_table[[col]] != ""], 3) cat(sprintf(" %s: %s\n", col, paste(sample_vals, collapse = ", "))) } # ❌ 不再自动检测基因符号列,由用户手动选择 cat("⚠️ 请用户手动选择基因符号列\n") # 返回原始表格,不进行自动检测和映射 return(list( success = TRUE, raw_table = raw_table, mapping = NULL, # 不提供自动映射 gene_symbol_col = NULL, # 不提供自动检测的列名 needs_manual_selection = TRUE, # 标记需要手动选择 message = "请手动选择ID列和基因列" )) }, error = function(e) { return(list( success = FALSE, error = paste("解析 SOFT 文件时出错:", e$message) )) }) } #' 智能检测基因符号列(改进版 - 基于内容分析) #' #' @param table 平台注释数据框 #' @return 字符列名或 NULL detect_gene_symbol_column <- function(table) { cat("🔍 开始智能检测基因符号列...\n") # 常见的基因符号列名(优先级高) high_priority_names <- c( "GENE_SYMBOL", "Gene.Symbol", "Gene_Symbol", "gene_symbol", "SYMBOL", "Symbol", "Gene Symbol" ) # 方法1: 优先级列名匹配 for (name in high_priority_names) { if (name %in% colnames(table)) { # 验证:检查该列是否真的包含基因符号格式 col_data <- table[[name]] col_data <- col_data[!is.na(col_data) & col_data != ""] n_check <- min(50, length(col_data)) if (n_check >= 10) { # 检查是否包含典型基因符号格式 # 基因符号:字母开头,可能包含数字和连字符,长度2-20 pattern <- "^[A-Z][A-Z0-9\\-]{1,15}$" match_ratio <- sum(grepl(pattern, col_data[1:n_check])) / n_check if (match_ratio > 0.5) { # 超过50%匹配 cat(sprintf("✅ 智能检测到基因列(高优先级): %s (匹配率: %.1f%%)\n", name, match_ratio*100)) return(name) } } } } # 方法2: 列名部分匹配 for (name in high_priority_names) { matching_cols <- colnames(table)[sapply(colnames(table), function(col) { grepl(name, col, ignore.case = TRUE) })] if (length(matching_cols) > 0) { # 验证内容 for (col_name in matching_cols) { col_data <- table[[col_name]] col_data <- col_data[!is.na(col_data) & col_data != ""] n_check <- min(50, length(col_data)) if (n_check >= 10) { pattern <- "^[A-Z][A-Z0-9\\-]{1,15}$" match_ratio <- sum(grepl(pattern, col_data[1:n_check])) / n_check if (match_ratio > 0.5) { cat(sprintf("✅ 智能检测到基因列(模糊匹配): %s (匹配率: %.1f%%)\n", col_name, match_ratio*100)) return(col_name) } } } } } # 方法3: 检查所有列的内容特征 best_col <- NULL best_match_ratio <- 0 for (col_name in colnames(table)) { # 跳过明显不是基因的列 if (col_name %in% c("ID", "SPOT_ID", "CONTROL_TYPE", "REFSEQ", "GB_ACC", "UNIGENE_ID", "ENSEMBL_ID", "TIGR_ID", "ACCESSION_STRING", "CHROMOSOMAL_LOCATION", "CYTOBAND", "DESCRIPTION", "GO_ID", "SEQUENCE")) { next } col_data <- table[[col_name]] col_data <- col_data[!is.na(col_data) & col_data != ""] n_check <- min(100, length(col_data)) if (n_check < 10) next # 检查基因符号特征 # 典型基因符号:TP53, EGFR, BRCA1, MYC, IL-6, TNF-alpha patterns <- c( "^[A-Z][A-Z0-9]{1,10}$", # 简单基因符号:TP53, EGFR "^[A-Z][A-Z0-9]{1,5}-[0-9]+$", # 带数字的:BRCA1-001 "^[A-Z]{2,6}-[A-Z0-9]{1,3}$" # 带连字符的:IL-6, TNF-a ) match_count <- sum(sapply(patterns, function(p) { sum(grepl(p, col_data[1:n_check])) })) match_ratio <- match_count / n_check if (match_ratio > best_match_ratio && match_ratio > 0.3) { best_match_ratio <- match_ratio best_col <- col_name } } if (!is.null(best_col)) { cat(sprintf("✅ 智能检测到基因列(内容分析): %s (匹配率: %.1f%%)\n", best_col, best_match_ratio*100)) return(best_col) } cat("⚠️ 无法自动检测基因符号列,需要用户手动选择\n") return(NULL) } #' 执行探针注释(合并表达矩阵和基因映射) #' #' @param expr_matrix 探针表达矩阵 #' @param probe_gene_map 探针-基因映射数据框 #' @return 基因表达矩阵 annotate_probe_matrix <- function(expr_matrix, probe_gene_map) { cat("🔄 开始探针注释...\n") # 找到共同的探针 common_probes <- intersect(rownames(expr_matrix), probe_gene_map$probe_id) if (length(common_probes) == 0) { stop("❌ 探针ID不匹配,请检查平台注释文件是否正确") } cat(sprintf("📊 共同探针数: %d (表达矩阵: %d, 注释文件: %d)\n", length(common_probes), nrow(expr_matrix), nrow(probe_gene_map))) # 提取共同探针的表达数据 expr_subset <- expr_matrix[common_probes, , drop = FALSE] # 添加探针ID列以便合并 expr_subset <- data.frame(probe_id = rownames(expr_subset), expr_subset, stringsAsFactors = FALSE) # 合并基因符号 merged <- merge(expr_subset, probe_gene_map, by = "probe_id") # 移除探针ID列 probe_id_col <- which(names(merged) == "probe_id") if (length(probe_id_col) > 0) { merged <- merged[, -probe_id_col] } # 按基因分组,对表达值取平均值 # 如果一个基因对应多个探针,取平均值 library(dplyr) expr_annotated <- merged %>% group_by(gene_symbol) %>% summarise(across(everything(), mean, na.rm = TRUE)) %>% column_to_rownames("gene_symbol") # 转换为矩阵 expr_annotated <- as.matrix(expr_annotated) cat(sprintf("✅ 探针注释完成: %d 探针 → %d 基因\n", length(common_probes), nrow(expr_annotated))) # 检查是否有基因被重复注释 n_probes_per_gene <- table(merged$gene_symbol) n_multi_probe_genes <- sum(n_probes_per_gene > 1) if (n_multi_probe_genes > 0) { cat(sprintf("📊 其中 %d 个基因有多个探针(已取平均)\n", n_multi_probe_genes)) } return(expr_annotated) } # ===================================================== # 3. limma 差异分析 # ===================================================== #' 使用 limma 进行芯片数据差异分析 #' #' @param expr_matrix 基因表达矩阵 #' @param ctrl_samples 对照组样本名 #' @param trt_samples 处理组样本名 #' @param pvalue_threshold P值阈值 #' @param logfc_threshold log2FC阈值 #' @param pval_type P值类型:"adj.P.Val" 或 "P.Value" #' @return 差异分析结果 run_limma_analysis <- function(expr_matrix, ctrl_samples, trt_samples, pvalue_threshold = 0.05, logfc_threshold = 1, pval_type = "adj.P.Val") { cat("🧬 开始 limma 差异分析...\n") # 检查样本 if (length(ctrl_samples) == 0 || length(trt_samples) == 0) { stop("❌ 对照组和处理组样本数不能为0") } # 重新排序矩阵(按照 Control -> Treatment) sample_order <- c(ctrl_samples, trt_samples) # 检查样本是否存在 missing_samples <- setdiff(sample_order, colnames(expr_matrix)) if (length(missing_samples) > 0) { stop(sprintf("❌ 以下样本不存在于表达矩阵中: %s", paste(missing_samples, collapse = ", "))) } expr_ordered <- expr_matrix[, sample_order, drop = FALSE] # 🆕 检查并移除非数值列(如ProbeID和Gene) # limma需要纯数值表达矩阵 if ("ProbeID" %in% colnames(expr_ordered)) { cat("📋 移除ProbeID列(非数值)\n") expr_ordered <- expr_ordered[, colnames(expr_ordered) != "ProbeID", drop = FALSE] } if ("Gene" %in% colnames(expr_ordered)) { cat("📋 移除Gene列(非数值)\n") expr_ordered <- expr_ordered[, colnames(expr_ordered) != "Gene", drop = FALSE] } # 确保所有列都是数值 expr_ordered <- as.matrix(expr_ordered) storage.mode(expr_ordered) <- "numeric" cat(sprintf("📊 最终分析矩阵: %d 基因 × %d 样本\n", nrow(expr_ordered), ncol(expr_ordered))) # 创建分组因子 group <- factor(c(rep("Control", length(ctrl_samples)), rep("Treatment", length(trt_samples)))) cat(sprintf("📊 样本分组: Control=%d, Treatment=%d\n", length(ctrl_samples), length(trt_samples))) # 加载 limma library(limma) # 设计矩阵 design <- model.matrix(~0 + group) colnames(design) <- levels(group) cat("📊 设计矩阵:\n") print(design) # 线性模型拟合 fit <- lmFit(expr_ordered, design) # 设置对比 contrast.matrix <- makeContrasts(Treatment-Control, levels=design) cat("📊 对比矩阵:\n") print(contrast.matrix) fit2 <- contrasts.fit(fit, contrast.matrix) fit2 <- eBayes(fit2) # 提取结果 results <- topTable(fit2, number = Inf, adjust.method = "BH", sort.by = "P") # 🔧 修复:正确设置ID和SYMBOL列 # rownames(results) 是基因符号(如"FAM174B", "TP53") # expr_matrix现在包含EntrezID列(如果有),用于ID列 results$SYMBOL <- rownames(results) # SYMBOL = 基因符号(行名) # ID列使用EntrezID(如果存在),否则使用基因符号 if ("EntrezID" %in% colnames(expr_matrix)) { # expr_matrix的行名对应results的行名 results$ID <- expr_matrix[rownames(results), "EntrezID"] # ID = Entrez Gene ID cat("✅ ID列使用Entrez Gene ID,SYMBOL列使用基因符号\n") } else { results$ID <- rownames(results) # 没有EntrezID时,ID也用基因符号 cat("✅ ID列和SYMBOL列都使用基因符号(无EntrezID)\n") } # 计算统计信息 - 根据用户选择的P值类型 n_total <- nrow(results) # 🔧 使用用户选择的P值类型 pval_col <- if (pval_type == "adj.P.Val") "adj.P.Val" else "P.Value" pval_values <- results[[pval_col]] n_significant <- sum(pval_values < pvalue_threshold, na.rm = TRUE) n_up <- sum(results$logFC > logfc_threshold & pval_values < pvalue_threshold, na.rm = TRUE) n_down <- sum(results$logFC < -logfc_threshold & pval_values < pvalue_threshold, na.rm = TRUE) cat(sprintf("✅ limma 分析完成: %d 个基因\n", n_total)) cat(sprintf(" 使用P值类型: %s\n", pval_type)) cat(sprintf(" 显著差异基因 (%s < %.3f): %d (%.1f%%)\n", pval_type, pvalue_threshold, n_significant, n_significant/n_total*100)) cat(sprintf(" 上调基因 (log2FC > %.2f): %d\n", logfc_threshold, n_up)) cat(sprintf(" 下调基因 (log2FC < %.2f): %d\n", logfc_threshold, n_down)) return(list( results = results, n_total = n_total, n_significant = n_significant, n_up = n_up, n_down = n_down, design = design, fit = fit2 )) } # ===================================================== # 4. 探针表达量聚合 # ===================================================== #' 聚合探针表达量到基因水平 #' #' @param probe_matrix 探针表达矩阵(行为探针,列为样本) #' @param probe_mapping 探针-基因映射数据框(从 parse_soft_platform 返回) #' @return 基因表达矩阵(行为基因,列为样本) aggregate_probe_expression <- function(probe_matrix, probe_mapping) { cat("🔄 开始聚合探针表达量...\n") # 提取映射关系 probe_ids <- probe_mapping$probe_id gene_symbols <- probe_mapping$gene_symbol # 创建映射向量(探针ID -> 基因符号) names(gene_symbols) <- probe_ids # 过滤掉未映射的探针 common_probes <- intersect(rownames(probe_matrix), probe_ids) if (length(common_probes) == 0) { cat("⚠️ 未找到匹配的探针ID\n") return(NULL) } cat(sprintf("✅ 匹配探针: %d / %d\n", length(common_probes), nrow(probe_matrix))) # 子集表达矩阵和映射 expr_subset <- probe_matrix[common_probes, , drop = FALSE] gene_symbols_subset <- gene_symbols[common_probes] # 移除NA和空字符串 valid_mask <- !is.na(gene_symbols_subset) & gene_symbols_subset != "" expr_subset <- expr_subset[valid_mask, , drop = FALSE] gene_symbols_subset <- gene_symbols_subset[valid_mask] # 聚合方法:对于每个基因,选择表达量最高的探针 cat("📊 聚合策略: 选择最高表达探针\n") # 获取唯一基因 unique_genes <- unique(gene_symbols_subset) cat(sprintf("📊 唯一基因数: %d\n", length(unique_genes))) # 为每个基因选择表达量最高的探针 gene_expr_list <- lapply(unique_genes, function(gene) { # 找到该基因的所有探针 gene_probes <- which(gene_symbols_subset == gene) if (length(gene_probes) == 1) { # 只有一个探针,直接使用 return(expr_subset[gene_probes, , drop = FALSE]) } else { # 多个探针,选择平均表达量最高的 avg_expr <- rowMeans(expr_subset[gene_probes, , drop = FALSE]) best_probe <- gene_probes[which.max(avg_expr)] return(expr_subset[best_probe, , drop = FALSE]) } }) # 合并为基因表达矩阵 gene_expr_matrix <- do.call(rbind, gene_expr_list) rownames(gene_expr_matrix) <- unique_genes cat(sprintf("✅ 聚合完成: %d 个基因\n", nrow(gene_expr_matrix))) return(gene_expr_matrix) } # ===================================================== # 5. 智能样本分组系统(通用版本) # ===================================================== #' 从 Series Matrix 元数据中自动检测分组 #' #' @param sample_names 样本名向量 #' @param sample_descriptions 样本描述向量(可选) #' @param sample_titles 样本标题向量(可选) #' @return list 包含分组建议和分组方法 detect_chip_groups_auto <- function(sample_names, sample_descriptions = NULL, sample_titles = NULL) { cat("🔍 开始自动检测分组模式...\n") cat(sprintf("📊 总样本数: %d\n", length(sample_names))) # 定义常见的分组模式 group_patterns <- list( # 模式1: 时间序列 (before/after, baseline/followup) list( name = "时间序列", ctrl_keywords = c("before", "baseline", "time0", "initial", "visit1"), trt_keywords = c("after", "post", "follow", "final", "visit2", "visit3") ), # 模式2: 处理对照 (control/treated) list( name = "处理对照", ctrl_keywords = c("control", "ctrl", "untreated", "vehicle", "placebo"), trt_keywords = c("treatment", "treated", "drug", "compound", "stimulated") ), # 模式3: 疾病对照 (normal/disease) list( name = "疾病对照", ctrl_keywords = c("normal", "healthy", "control", "wild"), trt_keywords = c("disease", "patient", "cancer", "tumor", "sick") ), # 模式4: 基因型 (wildtype/mutant) list( name = "基因型", ctrl_keywords = c("wild", "wt", "wildtype", "normal", "control"), trt_keywords = c("mutant", "mut", "knockout", "ko", "transgenic", "tg") ), # 模式5: 剂量反应 (control/dose) list( name = "剂量反应", ctrl_keywords = c("dose0", "dose_0", "control", "vehicle", "untreated"), trt_keywords = c("dose", "treatment", "low", "medium", "high") ), # 模式6: 激活/抑制 list( name = "激活抑制", ctrl_keywords = c("inactive", "unstimulated", "resting", "control"), trt_keywords = c("active", "stimulated", "induced", "activated") ) ) # 方法1: 从 sample_descriptions 检测 if (!is.null(sample_descriptions) && length(sample_descriptions) > 0) { cat("📋 尝试从 Sample_description 检测分组...\n") for (pattern in group_patterns) { ctrl_match <- sapply(sample_descriptions, function(d) { any(sapply(pattern$ctrl_keywords, function(kw) { grepl(kw, d, ignore.case = TRUE) })) }) trt_match <- sapply(sample_descriptions, function(d) { any(sapply(pattern$trt_keywords, function(kw) { grepl(kw, d, ignore.case = TRUE) })) }) # 检查是否找到匹配 if (sum(ctrl_match) > 0 && sum(trt_match) > 0) { ctrl_idx <- which(ctrl_match) trt_idx <- which(trt_match) cat(sprintf("✅ 检测到 '%s' 分组模式 (from description)\n", pattern$name)) cat(sprintf(" 对照组: %d 个样本 (%s)\n", length(ctrl_idx), paste(sample_names[ctrl_idx], collapse = ", "))) cat(sprintf(" 处理组: %d 个样本 (%s)\n", length(trt_idx), paste(sample_names[trt_idx], collapse = ", "))) return(list( pattern_name = pattern$name, method = "auto_description", ctrl_samples = sample_names[ctrl_idx], trt_samples = sample_names[trt_idx], ctrl_indices = ctrl_idx, trt_indices = trt_idx, confidence = "high", source = "description" )) } } } # 方法2: 从 sample_titles 检测 if (!is.null(sample_titles) && length(sample_titles) > 0) { cat("📋 尝试从 Sample_title 检测分组...\n") for (pattern in group_patterns) { ctrl_match <- sapply(sample_titles, function(t) { any(sapply(pattern$ctrl_keywords, function(kw) { grepl(kw, t, ignore.case = TRUE) })) }) trt_match <- sapply(sample_titles, function(t) { any(sapply(pattern$trt_keywords, function(kw) { grepl(kw, t, ignore.case = TRUE) })) }) if (sum(ctrl_match) > 0 && sum(trt_match) > 0) { ctrl_idx <- which(ctrl_match) trt_idx <- which(trt_match) cat(sprintf("✅ 检测到 '%s' 分组模式 (from title)\n", pattern$name)) cat(sprintf(" 对照组: %d 个样本\n", length(ctrl_idx))) cat(sprintf(" 处理组: %d 个样本\n", length(trt_idx))) return(list( pattern_name = pattern$name, method = "auto_title", ctrl_samples = sample_names[ctrl_idx], trt_samples = sample_names[trt_idx], ctrl_indices = ctrl_idx, trt_indices = trt_idx, confidence = "medium", source = "title" )) } } } # 方法3: 从样本名本身检测 cat("📋 尝试从样本名检测分组...\n") # 简化样本名(移除GSM前缀) simplified_names <- gsub("^GSM\\d+_", "", sample_names) for (pattern in group_patterns) { ctrl_match <- sapply(simplified_names, function(n) { any(sapply(pattern$ctrl_keywords, function(kw) { grepl(kw, n, ignore.case = TRUE) })) }) trt_match <- sapply(simplified_names, function(n) { any(sapply(pattern$trt_keywords, function(kw) { grepl(kw, n, ignore.case = TRUE) })) }) if (sum(ctrl_match) > 0 && sum(trt_match) > 0) { ctrl_idx <- which(ctrl_match) trt_idx <- which(trt_match) cat(sprintf("✅ 检测到 '%s' 分组模式 (from name)\n", pattern$name)) cat(sprintf(" 对照组: %d 个样本\n", length(ctrl_idx))) cat(sprintf(" 处理组: %d 个样本\n", length(trt_idx))) return(list( pattern_name = pattern$name, method = "auto_name", ctrl_samples = sample_names[ctrl_idx], trt_samples = sample_names[trt_idx], ctrl_indices = ctrl_idx, trt_indices = trt_idx, confidence = "medium", source = "name" )) } } # 所有方法都失败 cat("⚠️ 未能自动检测到分组模式,请手动设置\n") return(list( pattern_name = NULL, method = "manual", ctrl_samples = NULL, trt_samples = NULL, ctrl_indices = NULL, trt_indices = NULL, confidence = NULL, source = NULL )) } #' 检测配对关系 #' #' @param sample_names 样本名 #' @param metadata 元数据 #' @return list 配对关系或NULL detect_pairing <- function(sample_names, metadata) { # 尝试从样本名中提取配对信息 # 例如: Patient1_before, Patient1_after # 提取共同的配对标识符 # 方法:移除已知的对照组/处理组关键词后,看剩余部分是否匹配 # 简化样本名 simplified <- gsub("^GSM\\d+_", "", sample_names) # 定义配对关键词 pairing_keywords <- list( before = "after", baseline = "follow", control = "treated", time0 = "time1", visit1 = "visit2", pre = "post" ) # 尝试检测配对模式 for (kw1 in names(pairing_keywords)) { kw2 <- pairing_keywords[[kw1]] # 检查是否有样本包含这些关键词 has_kw1 <- grepl(kw1, simplified, ignore.case = TRUE) has_kw2 <- grepl(kw2, simplified, ignore.case = TRUE) if (sum(has_kw1) > 0 && sum(has_kw2) > 0) { # 提取配对标识符 ids_kw1 <- gsub(kw1, "", simplified[has_kw1], ignore.case = TRUE) ids_kw2 <- gsub(kw2, "", simplified[has_kw2], ignore.case = TRUE) # 找到共同的ID common_ids <- intersect(ids_kw1, ids_kw2) if (length(common_ids) > 0) { cat(sprintf("💡 检测到配对设计: %d 对样本\n", length(common_ids))) return(list( is_paired = TRUE, n_pairs = length(common_ids), pairing_pattern = sprintf("%s/%s", kw1, kw2) )) } } } return(list(is_paired = FALSE)) } # ===================================================== # 5. 转换为标准格式(与现有模块兼容) # ===================================================== #' 将芯片差异分析结果转换为标准格式 #' #' @param limma_results limma 分析结果 #' @param expr_matrix 表达矩阵 #' @param ctrl_samples 对照组样本 #' @param trt_samples 处理组样本 #' @return 标准格式的差异分析结果 format_chip_results_for_pipeline <- function(limma_results, expr_matrix, ctrl_samples, trt_samples) { # 🔧 修复:正确使用ID(EntrezID)和SYMBOL(基因符号)列 limma_res <- limma_results$results deg_df <- data.frame( ID = limma_res$ID, # Entrez Gene ID(如果有的话) SYMBOL = limma_res$SYMBOL, # 基因符号 log2FoldChange = limma_res$logFC, # log2倍数变化 pvalue = limma_res$P.Value, # 原始p值 padj = limma_res$adj.P.Val, # BH校正p值 baseMean = limma_res$AveExpr, # 平均表达 t = limma_res$t, # t统计量(用于TF活性分析) row.names = NULL, stringsAsFactors = FALSE ) # ENTREZID列与ID列相同(都是Entrez Gene ID) deg_df$ENTREZID <- deg_df$ID return(list( deg_df = deg_df, background_genes = rownames(expr_matrix), # 背景基因(用于富集分析) expr_matrix = expr_matrix, # 完整表达矩阵(用于 AUCell/GSVA) ctrl_samples = ctrl_samples, trt_samples = trt_samples, method = "limma", n_significant = limma_results$n_significant, n_up = limma_results$n_up, n_down = limma_results$n_down )) } # ===================================================== # 6. Shiny Server 函数 # ===================================================== #' 芯片数据分析模块 UI 生成函数 #' #' @return UI 元素 chip_analysis_ui <- function() { tagList( fluidRow( column(12, div( class = "info-box", style = "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 15px; margin-bottom: 25px;", h4("🧬 芯片数据分析模块", style = "margin-top: 0; color: white;"), p("支持 GEO Series Matrix 格式的芯片数据差异分析,自动探针注释,无缝集成下游富集分析。", style = "color: rgba(255,255,255,0.9); margin-bottom: 0;") ) ) ), # 🎨 使用可折叠面板组织所有步骤 tags$div( id = "chip_analysis_accordion", # ===== 面板1: 数据上传 ===== tags$div( class = "panel panel-default", tags$div( class = "panel-heading", style = "cursor: pointer; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px;", `data-toggle` = "collapse", `data-target` = "#panel_upload", tags$h4( class = "panel-title", style = "margin: 0;", tags$span(icon("upload"), " 📁 步骤1: 数据上传与预览") ) ), tags$div( id = "panel_upload", class = "panel-collapse collapse in", # in = 默认展开 wellPanel( # 文件上传 fluidRow( column(6, h5("📄 上传数据文件", style = "color: #007AFF;"), fileInput("chip_series_matrix", "GEO Series Matrix 文件", accept = c(".txt", ".matrix.txt", "text/plain"), placeholder = "选择文件..."), helpText("GEO 数据库下载的 Series Matrix 文件(通常包含样本表达矩阵)") ), column(6, fileInput("chip_soft_platform", "SOFT 平台注释文件 (可选)", accept = c(".txt", ".soft", "annot.txt", "text/plain"), placeholder = "选择文件..."), helpText("用于探针注释的 GPL 平台文件。如不上传,系统将尝试自动注释。") ) ), tags$hr(style = "border-color: #dee2e6;"), # 数据预览 h5("📊 数据文件预览", style="color: #007AFF;"), fluidRow( column(6, h6("Series Matrix 文件(前5行)", style="color: #666;"), DTOutput("chip_series_matrix_preview") ), column(6, h6("SOFT 文件(前10行)", style="color: #666;"), DTOutput("chip_soft_raw_preview") ) ) ), tags$div( class = "panel-body", style = "padding: 15px;" ) ) ), # ===== 面板2: 探针注释配置 ===== tags$div( class = "panel panel-default", tags$div( class = "panel-heading", style = "cursor: pointer; background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%); color: white; padding: 15px;", `data-toggle` = "collapse", `data-target` = "#panel_annotation", tags$h4( class = "panel-title", style = "margin: 0;", tags$span(icon("cogs"), " 🧬 步骤2: 探针注释与数据合并") ) ), tags$div( id = "panel_annotation", class = "panel-collapse collapse", # 默认折叠 wellPanel( # SOFT文件列名清单 uiOutput("chip_soft_columns_list_ui"), tags$hr(style = "border-color: #dee2e6;"), # 探针注释配置 h5("📋 探针注释配置", style = "color: #9C27B0;"), uiOutput("chip_soft_column_selection_panel"), tags$hr(style = "border-color: #dee2e6;"), # 合并操作按钮 fluidRow( column(6, actionButton("chip_preview_merge", "👁️ 预览合并结果", class = "btn-info", style = "width: 100%;") ), column(6, actionButton("chip_apply_merge", "✅ 应用配置并生成最终矩阵", class = "btn-success", style = "width: 100%;") ) ), # 合并预览 conditionalPanel( condition = "input.chip_preview_merge", wellPanel( style = "background: #e8f5e9; border: 2px solid #4caf50;", h5("👁️ 合并结果预览(前5行)", style = "color: #2e7d32;"), DTOutput("chip_merge_preview_table") ) ), # 最终矩阵显示 conditionalPanel( condition = "input.chip_apply_merge", wellPanel( style = "background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); border: 2px solid #4caf50;", h5("✅ 最终表达矩阵", style = "color: #2e7d32; font-size: 18px; font-weight: bold;"), uiOutput("chip_final_matrix_ui"), br(), helpText("💡 此矩阵可直接用于后续的差异分析。已将探针ID和基因符号合并到表达数据中。", style = "color: #2e7d33;") ) ) ) ), # ===== 面板3: 数据预处理与探针去重 ===== tags$div( class = "panel panel-default", tags$div( class = "panel-heading", style = "cursor: pointer; background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); color: white; padding: 15px;", `data-toggle` = "collapse", `data-target` = "#panel_preprocess", tags$h4( class = "panel-title", style = "margin: 0;", tags$span(icon("sliders-h"), " 🔧 步骤3: 数据预处理与探针去重") ) ), tags$div( id = "panel_preprocess", class = "panel-collapse collapse in", # 默认展开 wellPanel( # 预处理 h5("📊 数据预处理(log2转换 + 标准化)", style = "color: #ff9800; font-weight: bold;"), fluidRow( column(6, checkboxInput("chip_auto_log2", "自动判断并执行log2转换", value = TRUE), checkboxInput("chip_normalize_data", "执行limma标准化(normalizeBetweenArrays)", value = TRUE) ), column(6, actionButton("chip_preprocess_data", "⚙️ 执行预处理", class = "btn-warning", style = "width: 100%;") ) ), # 预处理结果 conditionalPanel( condition = "input.chip_preprocess_data", uiOutput("chip_preprocess_result_ui") ), tags$hr(style = "border-color: #ffc107;"), # 批次矫正 h5("🎛️ 批次效应矫正(可选)", style = "color: #E91E63; font-weight: bold;"), fluidRow( column(6, selectInput("chip_batch_method", "批次矫正方法", choices = c("无" = "none", "ComBat (sva)" = "combat", "ComBat (limma)" = "limma", "SVA" = "sva"), selected = "none") ), column(6, actionButton("chip_apply_batch_correct", "🎛️ 执行批次矫正", class = "btn-danger", style = "width: 100%;") ) ), # 批次矫正结果 conditionalPanel( condition = "input.chip_apply_batch_correct", uiOutput("chip_batch_correct_result_ui") ), tags$hr(style = "border-color: #ffc107;"), # 探针去重 h5("✂️ 探针去重(保留表达量最高的探针)", style = "color: #ff9800; font-weight: bold;"), helpText("当一个基因对应多个探针时,保留表达量最高的探针。这将生成基因级别的表达矩阵。"), fluidRow( column(6, h6("去重前统计:", style = "color: #666;"), uiOutput("chip_before_dedupe_stats") ), column(6, h6("去重后统计:", style = "color: #666;"), uiOutput("chip_after_dedupe_stats") ) ), actionButton("chip_dedupe_probes", "✂️ 执行探针去重", class = "btn-warning btn-lg", style = "width: 100%;"), # 去重结果 conditionalPanel( condition = "input.chip_dedupe_probes", wellPanel( h6("✅ 去重完成", style = "color: #28a745;"), uiOutput("chip_dedupe_result_ui") ) ), tags$hr(style = "border-color: #ffc107;"), # 生成标准格式数据 h5("💾 生成标准格式数据", style = "color: #ff9800; font-weight: bold;"), helpText("将处理后的表达矩阵转换成标准格式,可直接用于后续的差异分析、KEGG、GO等模块。"), fluidRow( column(12, div( style = "background: #d4edda; padding: 15px; border-radius: 8px; border: 1px solid #c3e6cb;", h6("💡 即可对接现有分析模块:", style = "color: #155724;"), tags$ul(style="margin: 10px 0; padding-left: 20px;", tags$li("差异分析(使用现有的差异分析模块)"), tags$li("KEGG富集分析(使用现有的KEGG模块)"), tags$li("GO富集分析(使用现有的GO模块)"), tags$li("GSEA分析(使用现有的GSEA模块)") ) ) ) ), actionButton("chip_generate_standard_data", "🚀 生成标准格式数据", class = "btn-success btn-lg", style = "width: 100%; font-size: 16px;") ) ) ), # ===== 面板4: 差异分析 ===== tags$div( class = "panel panel-default", tags$div( class = "panel-heading", style = "cursor: pointer; background: linear-gradient(135deg, #34C759 0%, #2e7d32 100%); color: white; padding: 15px;", `data-toggle` = "collapse", `data-target` = "#panel_diff_analysis", tags$h4( class = "panel-title", style = "margin: 0;", tags$span(icon("chart-bar"), " 🧬 步骤4: 差异分析") ) ), tags$div( id = "panel_diff_analysis", class = "panel-collapse collapse", # 默认折叠 style = "padding: 15px;", wellPanel( # 样本分组 uiOutput("chip_grouping_ui"), tags$hr(style = "border-color: #34C759;"), # 差异分析参数 h5("🔬 差异分析参数", style = "color: #34C759; font-weight: bold;"), fluidRow( column(4, sliderInput("chip_logfc_threshold", "log2FoldChange 阈值:", min = 0, max = 5, value = 1, step = 0.1) ), column(4, selectInput("chip_pval_type", "显著性指标:", choices = c("校正P值 (adj.P.Val)" = "adj.P.Val", "原始P值 (P.Value)" = "P.Value"), selected = "adj.P.Val") ), column(4, sliderInput("chip_pvalue_threshold", "P值 阈值:", min = 0.001, max = 0.1, value = 0.05, step = 0.001) ) ), fluidRow( column(12, checkboxInput("chip_paired_analysis", "配对样本分析(如果适用)", value = FALSE) ) ), tags$hr(style = "border-color: #34C759;"), actionButton("run_chip_analysis", "🚀 运行差异分析", class = "btn-primary btn-lg", style = "width: 100%; margin-top: 15px;") ) ) ) ), # ===== 面板5: 分析结果 ===== tags$div( class = "panel panel-default", tags$div( class = "panel-heading", style = "cursor: pointer; background: linear-gradient(135deg, #007AFF 0%, #0051D5 100%); color: white; padding: 15px;", `data-toggle` = "collapse", `data-target` = "#panel_results", tags$h4( class = "panel-title", style = "margin: 0;", tags$span(icon("table"), " 📊 步骤5: 分析结果") ) ), tags$div( id = "panel_results", class = "panel-collapse collapse", # 默认折叠 style = "padding: 15px;", wellPanel( # 结果统计和表格(chip_results_ui 已包含表格) uiOutput("chip_results_ui"), tags$hr(style = "border-color: #007AFF;"), # 下载按钮 fluidRow( column(12, downloadButton("download_chip_results", "📥 下载结果", class = "btn-success") ) ) ) ) ) ) ) } #' 芯片数据分析模块 Server 函数 #' #' @param input Shiny input #' @param output Shiny output #' @param session Shiny session #' @param deg_results 差异分析结果容器(用于更新) chip_analysis_server <- function(input, output, session, deg_results) { cat("✅ 芯片分析模块已启动\n") # 渲染 UI output$chip_analysis_ui_output <- renderUI({ chip_analysis_ui() }) # 存储芯片数据 chip_data <- reactiveValues( series_matrix = NULL, soft_platform = NULL, probe_mapping = NULL, expr_matrix = NULL, metadata = NULL, group_info = NULL, manual_ctrl_samples = NULL, manual_trt_samples = NULL ) # 解析对照组样本 observeEvent(input$chip_parse_ctrl, { req(input$chip_paste_ctrl) req(chip_data$series_matrix) pasted_text <- input$chip_paste_ctrl # 显示进度 showNotification("正在解析对照组样本...", type = "message") # 解析样本列表 samples <- parse_sample_list(pasted_text, chip_data$series_matrix) if (is.null(samples) || length(samples) == 0) { showNotification("解析失败:未找到有效样本", type = "error") return(NULL) } # 保存对照组 chip_data$manual_ctrl_samples <- samples showNotification( sprintf("✅ 成功解析对照组: %d 个样本", length(samples)), type = "message" ) }) # 解析处理组样本 observeEvent(input$chip_parse_trt, { req(input$chip_paste_trt) req(chip_data$series_matrix) pasted_text <- input$chip_paste_trt # 显示进度 showNotification("正在解析处理组样本...", type = "message") # 解析样本列表 samples <- parse_sample_list(pasted_text, chip_data$series_matrix) if (is.null(samples) || length(samples) == 0) { showNotification("解析失败:未找到有效样本", type = "error") return(NULL) } # 保存处理组 chip_data$manual_trt_samples <- samples showNotification( sprintf("✅ 成功解析处理组: %d 个样本", length(samples)), type = "message" ) }) # 清除分组设置 observeEvent(input$chip_clear_groups, { chip_data$manual_ctrl_samples <- NULL chip_data$manual_trt_samples <- NULL showNotification("已清除分组设置", type = "message") }) # 解析 Series Matrix 文件 observeEvent(input$chip_series_matrix, { req(input$chip_series_matrix) file_path <- input$chip_series_matrix$datapath # 显示进度 showNotification("正在解析 GEO Series Matrix 文件...", type = "message") # 解析文件 result <- parse_geo_series_matrix(file_path) if (!result$success) { showNotification(result$error, type = "error") return(NULL) } # 保存数据 chip_data$series_matrix <- result$matrix chip_data$metadata <- result$metadata chip_data$expr_matrix <- result$matrix showNotification( sprintf("✅ 成功解析: %d 探针 × %d 样本", result$n_probes, result$n_samples), type = "message" ) # 尝试自动检测分组 if (!is.null(result$metadata)) { group_info <- detect_chip_groups_auto( sample_names = result$sample_names, sample_descriptions = result$metadata$sample_descriptions, sample_titles = result$metadata$sample_titles ) chip_data$group_info <- group_info } }, ignoreNULL = TRUE) # Series Matrix 文件预览 output$chip_series_matrix_preview <- renderDT({ req(chip_data$series_matrix) # 提取前5行 preview_matrix <- head(chip_data$series_matrix, 5) # 转换为数据框(保留行名) preview_df <- as.data.frame(preview_matrix) datatable( preview_df, options = list( dom = 't', paging = FALSE, scrollX = TRUE, columnDefs = list(list( className = 'dt-center', targets = "_all" )) ), rownames = TRUE, # 显示行名(探针ID) filter = 'none' ) %>% formatStyle(columns = 1:min(5, ncol(preview_df)), fontSize = '85%') }) # SOFT 文件预览UI output$chip_soft_preview_ui <- renderUI({ # 只要有文件上传记录就显示预览 if (!is.null(input$chip_soft_platform)) { cat(sprintf("🔍 渲染SOFT预览UI (文件已上传)\n")) if (!is.null(chip_data$soft_platform)) { cat(sprintf(" 数据可用: %d rows x %d cols\n", nrow(chip_data$soft_platform), ncol(chip_data$soft_platform))) } else { cat(" ⚠️ 数据尚未加载到chip_data\n") } tagList( h5("📄 SOFT 平台注释文件(前10行)", style = "color: #FF9800;"), helpText("这是SOFT注释文件的真实数据。可以看到ID列和多个可能的基因列。"), DTOutput("chip_soft_raw_preview") ) } else { cat("⚠️ SOFT文件未上传\n") div( class = "alert alert-info", h5("📄 请先上传SOFT平台注释文件"), p("上传后将在此处显示文件预览。") ) } }) # 🆕 SOFT文件列名列表展示 output$chip_soft_columns_list_ui <- renderUI({ req(chip_data$soft_platform) soft_cols <- colnames(chip_data$soft_platform) tagList( h6("📌 所有列名(共", span(style = "color: #FF9500;", length(soft_cols)), "列):", style = "color: #333;"), # 以表格形式展示所有列名 div( style = "background: #fff3e0; padding: 15px; border-radius: 8px; border-left: 4px solid #FF9800;", # 每行显示3列 div(style = "display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;", lapply(seq_along(soft_cols), function(i) { col_name <- soft_cols[i] div( style = "background: white; padding: 8px; border-radius: 4px; border: 1px solid #FFB74D;", tags$span(style = "color: #E65100; font-weight: bold; font-family: monospace;", sprintf("%d. %s", i, col_name)) ) }) ) ), br(), # 重要提示 div( style = "background: #e3f2fd; padding: 12px; border-radius: 5px; border-left: 4px solid #2196F3;", h6("💡 如何选择基因列?", style = "color: #1976D2; margin-top: 0;"), tags$ul(style = "padding-left: 20px; margin: 5px 0;", tags$li("查看下方的列内容示例,了解每列包含什么数据"), tags$li("基因符号列通常包含:TP53, EGFR, BRCA1, MYC 等基因名称"), tags$li("ID列通常包含:数字或探针标识符(如 1553601_at)"), tags$li("点击下方的'查看列内容'按钮查看每列的前5行数据") ) ) ) }) # 🆕 显示各列内容示例 output$chip_soft_column_examples_ui <- renderUI({ req(chip_data$soft_platform) soft_cols <- colnames(chip_data$soft_platform) # 为每一列生成示例展示 tagList( h6("📊 各列内容示例(前3行)", style = "color: #333;"), div(style = "max-height: 400px; overflow-y: auto;"), lapply(soft_cols, function(col_name) { # 获取该列的前3个非空值 col_data <- chip_data$soft_platform[[col_name]] col_data <- col_data[!is.na(col_data) & col_data != ""] examples <- head(col_data, 3) wellPanel( style = "padding: 10px; margin-bottom: 10px;", h7(style = "color: #FF9800; font-weight: bold; margin-bottom: 5px;", sprintf("🔹 %s", col_name)), div( style = "background: #f5f5f5; padding: 8px; border-radius: 4px; font-family: monospace; font-size: 11px;", for (i in seq_along(examples)) { tags$div( sprintf(" %d. %s", i, as.character(examples[i])), style = i < length(examples) ? "margin-bottom: 5px;" : "" ) } ) ) }) ) }) # 解析 SOFT 平台文件 observeEvent(input$chip_soft_platform, { req(input$chip_soft_platform) file_path <- input$chip_soft_platform$datapath showNotification("正在解析 SOFT 平台注释文件...", type = "message") result <- parse_platform_annotation(file_path, "\t") # 使用Tab作为分隔符 if (!result$success) { showNotification(result$error, type = "error") return(NULL) } chip_data$soft_platform <- result$raw_table chip_data$probe_mapping <- NULL # 不使用自动映射 chip_data$gene_symbol_col <- NULL # 不使用自动检测的列名 cat(sprintf("💾 SOFT数据已保存: %d rows x %d cols\n", nrow(chip_data$soft_platform), ncol(chip_data$soft_platform))) cat("⚠️ 请用户手动选择ID列和基因列\n") showNotification( "✅ SOFT文件已加载,请在下方手动选择ID列和基因列", type = "message" ) }, ignoreNULL = TRUE) # 重新解析SOFT文件(修改分隔符后) observeEvent(input$chip_reparse_soft, { req(input$chip_soft_platform) file_path <- input$chip_soft_platform$datapath # 获取分隔符,默认为Tab separator <- if (is.null(input$chip_soft_separator) || input$chip_soft_separator == "") { "\t" } else { input$chip_soft_separator } showNotification("正在重新解析 SOFT 平台注释文件...", type = "message") result <- parse_platform_annotation(file_path, separator) if (!result$success) { showNotification(result$error, type = "error") return(NULL) } chip_data$soft_platform <- result$raw_table chip_data$probe_mapping <- NULL # 不使用自动映射 chip_data$gene_symbol_col <- NULL # 不使用自动检测的列名 showNotification( "✅ 重新解析完成,请手动选择ID列和基因列", type = "message" ) }) # 数据概览 output$chip_data_summary <- renderDT({ req(chip_data$series_matrix) matrix <- chip_data$series_matrix # 创建摘要表 summary_df <- data.frame( 项目 = c("探针数", "样本数", "样本名称"), 值 = c( nrow(matrix), ncol(matrix), paste(colnames(matrix), collapse = ", ") ) ) # 添加分组信息 if (!is.null(chip_data$group_info) && !is.null(chip_data$group_info$pattern_name)) { summary_df <- rbind(summary_df, data.frame( 项目 = c("检测到分组模式", "对照组样本", "处理组样本"), 值 = c( chip_data$group_info$pattern_name, paste(chip_data$group_info$ctrl_samples, collapse = ", "), paste(chip_data$group_info$trt_samples, collapse = ", ") ) )) } datatable(summary_df, options = list(dom = 't', paging = FALSE), rownames = FALSE) }) # 注释状态显示 output$chip_annotation_status <- renderUI({ req(chip_data$series_matrix) # 检查是否加载了SOFT文件 soft_loaded <- !is.null(chip_data$soft_platform) if (soft_loaded) { # 已加载SOFT文件但尚未配置映射 div( class = "alert alert-info", h5("✅ SOFT文件已加载,等待配置", style = "color: #17a2b8;"), p(sprintf("总探针数: %d", nrow(chip_data$series_matrix))), p(sprintf("SOFT平台数据: %d 行 x %d 列", nrow(chip_data$soft_platform), ncol(chip_data$soft_platform))), p("💡 请在下方选择ID列和基因列以建立探针映射。", style = "color: #007bff; font-weight: bold;"), if (!is.null(chip_data$probe_mapping)) { p(sprintf("成功映射: %d (%.1f%%)", nrow(chip_data$probe_mapping), nrow(chip_data$probe_mapping) / nrow(chip_data$series_matrix) * 100)) } ) } else { # 未加载SOFT文件 div( class = "alert alert-warning", h5("⚠️ 未加载探针注释文件", style = "color: #ffc107;"), p("将直接使用探针ID作为基因符号进行分析。"), p("强烈建议上传 SOFT 平台注释文件以获得准确的结果。") ) } }) # 🆕 SOFT文件列选择面板(使用renderUI而不是conditionalPanel) output$chip_soft_column_selection_panel <- renderUI({ # 检查SOFT文件是否加载 if (is.null(chip_data$soft_platform)) { return(NULL) } # 只打印一次日志,避免刷屏 if (is.null(chip_data$panel_initialized)) { cat(sprintf("✅ 初始化SOFT列选择面板: %d 行 x %d 列\n", nrow(chip_data$soft_platform), ncol(chip_data$soft_platform))) chip_data$panel_initialized <- TRUE } # 使用isolate防止input变化触发重新渲染 soft_cols <- isolate(colnames(chip_data$soft_platform)) # 获取当前选择值(使用isolate避免建立依赖) current_id <- isolate({ if (!is.null(input$chip_soft_id_col) && input$chip_soft_id_col != "") { input$chip_soft_id_col } else { "" } }) current_gene <- isolate({ if (!is.null(input$chip_soft_gene_col) && input$chip_soft_gene_col != "") { input$chip_soft_gene_col } else { "" } }) wellPanel( style = "background: linear-gradient(135deg, #fff7e6 0%, #ffe6b3 100%); border: 2px solid #FF9800;", h4("📋 SOFT文件列选择", style = "color: #FF9800; margin-top: 0;"), helpText("💡 您可以预先浏览和选择SOFT文件的列,即使还未上传Series Matrix文件。"), br(), fluidRow( column(4, h5("选择ID列", style = "color: #9C27B0; font-weight: bold;"), selectInput("chip_soft_id_col", "选择ID列(必须与Series Matrix的探针ID匹配)", choices = c("", soft_cols), selected = current_id), helpText("选择SOFT文件中包含探针ID的列(通常为'ID'列)") ), column(4, h5("选择基因列", style = "color: #9C27B0; font-weight: bold;"), selectInput("chip_soft_gene_col", "选择基因列(包含基因符号的列)", choices = c("", soft_cols), selected = current_gene), helpText("选择包含基因符号的列(如GENE_SYMBOL, SYMBOL)") ), column(4, h5("选择EntrezID列(可选)", style = "color: #FF5722; font-weight: bold;"), selectInput("chip_soft_entrez_col", "选择Entrez Gene ID列", choices = c("", "自动检测", soft_cols), selected = ""), helpText("选择包含Entrez Gene ID的列(如GENE, ENTREZID)", style = "font-size: 11px; color: #FF5722;") ) ), # 状态提示单独的uiOutput,避免循环 uiOutput("chip_selection_status") ) }) # 🆕 状态提示单独渲染(避免导致父级重新渲染) output$chip_selection_status <- renderUI({ # 使用isolate避免触发父级renderUI id_col <- isolate(input$chip_soft_id_col) gene_col <- isolate(input$chip_soft_gene_col) entrez_col <- isolate(input$chip_soft_entrez_col) # 🔍 自动检测可能的EntrezID列 possible_entrez_hint <- "" if (!is.null(chip_data$soft_platform)) { soft_cols <- colnames(chip_data$soft_platform) # 检查是否有常见的EntrezID列名 entrez_candidates <- c("ENTREZ_GENE_ID", "ENTREZID", "EntrezID", "GeneID", "GENE_ID", "ENTREZ_GENE", "GENE") found_cols <- intersect(entrez_candidates, soft_cols) if (length(found_cols) > 0) { possible_entrez_hint <- p(sprintf("💡 检测到可能的EntrezID列: %s", paste(found_cols, collapse = ", ")), collapse = " ") } } if (is.null(id_col) || id_col == "" || is.null(gene_col) || gene_col == "") { div( style = "background: #fff3cd; padding: 10px; border-radius: 5px; margin-top: 15px;", h6("⚠️ 未完成选择", style = "color: #856404;"), p("请选择ID列和基因列,然后上传Series Matrix文件并点击应用按钮。", style = "font-size: 12px;"), if (possible_entrez_hint != "") { p(possible_entrez_hint, style = "font-size: 11px; color: #FF5722; font-weight: bold;") } ) } else { status_color <- "d4edda" status_text <- "✅ 已选择列" status_text_color <- "155724" # 检查是否选择了EntrezID列 if (is.null(entrez_col) || entrez_col == "") { if (possible_entrez_hint != "") { status_color <- "fff3cd" status_text <- "⚠️ 建议选择EntrezID列" status_text_color <- "856404" } } div( style = sprintf("background: %s; padding: 10px; border-radius: 5px; margin-top: 15px;", status_color), h6(status_text, style = sprintf("color: %s;", status_text_color)), p(sprintf("ID列: %s | 基因列: %s", id_col, gene_col), style = "font-size: 12px; font-weight: bold;"), if (!is.null(entrez_col) && entrez_col != "" && entrez_col != "自动检测") { p(sprintf("EntrezID列: %s", entrez_col), style = "font-size: 12px; color: #FF5722; font-weight: bold;") }, p("💡 请上传Series Matrix文件,然后点击'✅ 应用配置并生成最终矩阵'按钮。", style = "font-size: 12px;"), if (possible_entrez_hint != "" && (is.null(entrez_col) || entrez_col == "")) { p(possible_entrez_hint, style = "font-size: 11px; color: #FF5722; font-weight: bold; margin-top: 5px;" ) } ) } }) # 🆕 监听列选择(只在日志中记录,不触发UI更新) observe({ # 只有两个列都选择了才处理 req(input$chip_soft_id_col) req(input$chip_soft_gene_col) req(input$chip_soft_id_col != "") req(input$chip_soft_gene_col != "") # 保存选择到chip_data chip_data$selected_id_col <- input$chip_soft_id_col chip_data$selected_gene_col <- input$chip_soft_gene_col # 使用isolate避免触发UI重新渲染 isolate({ cat(sprintf("📋 用户已选择: ID列=%s, 基因列=%s\n", input$chip_soft_id_col, input$chip_soft_gene_col)) }) }) # 🆕 合并工作流面板renderUI(替代conditionalPanel) output$chip_merge_workflow_panel <- renderUI({ # 检查两个文件是否都已上传 has_series <- !is.null(chip_data$series_matrix) has_soft <- !is.null(chip_data$soft_platform) if (!has_series || !has_soft) { return(NULL) } # 检查是否已选择列 has_id_col <- !is.null(input$chip_soft_id_col) && input$chip_soft_id_col != "" has_gene_col <- !is.null(input$chip_soft_gene_col) && input$chip_soft_gene_col != "" wellPanel( style = "background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border: 2px solid #667eea;", h4("🔗 探针注释与数据合并工作流", style = "color: #667eea; margin-top: 0;"), # 工作流说明 div( style = "background: rgba(255,255,255,0.8); padding: 15px; border-radius: 8px; margin-bottom: 20px;", h5("📋 重要提示", style = "color: #dc3545;"), helpText("您已在上方黄色面板选择了列,现在可以应用配置生成最终矩阵!"), if (has_id_col && has_gene_col) { tags$div(style = "background: #d4edda; padding: 10px; border-radius: 5px; border-left: 4px solid #28a745;", tags$strong("✅ 当前配置:"), tags$ul(style = "margin: 10px 0;", tags$li(sprintf("ID列: %s", input$chip_soft_id_col)), tags$li(sprintf("基因列: %s", input$chip_soft_gene_col)) ) ) } else { tags$div(style = "background: #fff3cd; padding: 10px; border-radius: 5px; border-left: 4px solid #ffc107;", tags$strong("⚠️ 请先在上方黄色面板选择列!") ) } ), # 应用按钮 if (has_id_col && has_gene_col) { tagList( hr(style = "border-color: #667eea;"), h5("步骤5: 应用配置并生成最终矩阵", style = "color: #9C27B0; font-weight: bold;"), fluidRow( column(12, actionButton("chip_apply_merge", "✅ 应用配置并生成最终矩阵", class = "btn-success btn-lg btn-block", style = "font-size: 16px; padding: 15px;"), helpText("点击后将应用所有配置,生成带基因符号的表达矩阵。", style = "text-align: center;") ) ) ) } ) }) # SOFT文件列选择UI output$chip_soft_columns_ui <- renderUI({ req(chip_data$soft_platform) soft_cols <- colnames(chip_data$soft_platform) tagList( h5("🔍 SOFT文件列信息", style = "color: #9C27B0;"), p(sprintf("检测到 %d 列,请确认基因符号列:", ncol(chip_data$soft_platform))), fluidRow( column(8, selectInput("chip_gene_symbol_col", "选择基因符号列:", choices = soft_cols, selected = chip_data$gene_symbol_col) ), column(4, br(), actionButton("chip_update_gene_col", "🔄 更新列选择", class = "btn-primary", style = "width: 100%;") ) ), helpText("💡 提示:系统已自动检测,如不正确可手动选择。", class = "text-info") ) }) # 更新基因符号列 observeEvent(input$chip_update_gene_col, { req(chip_data$soft_platform) req(input$chip_gene_symbol_col) selected_col <- input$chip_gene_symbol_col # 重新提取映射 probe_col <- chip_data$soft_platform[, 1] # 假设第一列是探针ID gene_col <- chip_data$soft_platform[, selected_col] # 移除NA和空 valid_mask <- !is.na(gene_col) & gene_col != "" probe_col <- probe_col[valid_mask] gene_col <- gene_col[valid_mask] # 创建映射 mapping <- data.frame( probe_id = as.character(probe_col), gene_symbol = as.character(gene_col), stringsAsFactors = FALSE ) chip_data$probe_mapping <- mapping chip_data$gene_symbol_col <- selected_col showNotification( sprintf("✅ 已更新基因列为: %s (%d 个映射)", selected_col, nrow(mapping)), type = "message" ) }) # SOFT文件原始数据预览 output$chip_soft_raw_preview <- renderDT({ req(chip_data$soft_platform) # 显示前10行,但限制显示的列数 preview_df <- head(chip_data$soft_platform, 10) # 🔧 截断过长的文本以避免渲染问题 truncate_text <- function(text, max_len = 100) { if (is.character(text) || is.factor(text)) { text <- as.character(text) text <- ifelse(nchar(text) > max_len, paste0(substr(text, 1, max_len), "..."), text) } return(text) } # 对所有列应用截断 for (col in colnames(preview_df)) { preview_df[[col]] <- truncate_text(preview_df[[col]], max_len = 100) } # 限制显示的列数(最多显示15列,避免表格过宽) if (ncol(preview_df) > 15) { preview_df <- preview_df[, 1:15] cat(sprintf("⚠️ SOFT文件有%d列,仅显示前15列\n", ncol(chip_data$soft_platform))) } datatable( preview_df, options = list( dom = 't', paging = FALSE, scrollX = TRUE, scrollY = "400px", columnDefs = list(list( className = 'dt-center', targets = "_all" )) ), rownames = FALSE, filter = 'none', escape = FALSE # 允许HTML渲染(用于显示省略号) ) %>% formatStyle(columns = 1:ncol(preview_df), fontSize = '85%', maxWidth = '200px', overflow = 'hidden', textOverflow = 'ellipsis') }) # 探针-基因映射预览 output$chip_probe_mapping_preview <- renderDT({ req(chip_data$probe_mapping) # 显示前10行 preview_df <- head(chip_data$probe_mapping, 10) datatable( preview_df, options = list(dom = 't', paging = FALSE), rownames = FALSE, colnames = c("探针ID", "基因符号") ) %>% formatStyle(columns = c("探针ID", "基因符号"), fontSize = '90%') }) # ============================================ # 🆕 探针注释与合并工作流 - 服务端逻辑 # ============================================ # 步骤2: 渲染SOFT文件ID列选择器 output$chip_soft_id_column_ui <- renderUI({ req(chip_data$soft_platform) soft_cols <- colnames(chip_data$soft_platform) tagList( selectInput("chip_soft_id_col", "选择ID列(必须与Series Matrix的探针ID匹配)", choices = c("", soft_cols), # 添加空选项 selected = ""), helpText("💡 提示:根据上方SOFT文件预览,选择包含探针ID的列(通常为'ID'列)", style = "color: #ff9800;") ) }) # 步骤3: 渲染SOFT文件基因列选择器 output$chip_soft_gene_column_ui <- renderUI({ req(chip_data$soft_platform) soft_cols <- colnames(chip_data$soft_platform) tagList( selectInput("chip_soft_gene_col", "选择基因列(包含基因符号的列)", choices = c("", soft_cols), # ✅ 添加空选项 selected = ""), # ✅ 默认不选中,强制用户手动选择 helpText("⚠️ 请根据上方SOFT文件预览和右侧数据示例,手动选择包含基因符号的列(如 GENE_SYMBOL)。", style = "color: #ff9800; font-weight: bold;") ) }) # 步骤4: 显示选中基因列的实际示例数据 output$chip_gene_column_examples_ui <- renderUI({ req(chip_data$soft_platform) req(input$chip_soft_gene_col) gene_col <- input$chip_soft_gene_col examples <- head(chip_data$soft_platform[[gene_col]], 5) tagList( h6("📋 实际数据示例(前5行):", style = "color: #667eea;"), div( style = "background: #f8f9fa; padding: 10px; border-radius: 5px; font-family: monospace; font-size: 11px;", for (i in seq_along(examples)) { tags$div( sprintf("%d. %s", i, as.character(examples[i])) ) } ) ) }) # 步骤4: 正则表达式测试 output$chip_regex_test_result_ui <- renderUI({ req(input$chip_test_regex) # 获取测试数据 example_text <- if (!is.null(input$chip_gene_extract_example) && input$chip_gene_extract_example != "") { input$chip_gene_extract_example } else { # 使用实际数据示例 req(input$chip_soft_gene_col) examples <- head(chip_data$soft_platform[[input$chip_soft_gene_col]], 3) paste(examples, collapse = "\n") } regex_pattern <- input$chip_gene_regex %||% "[A-Z][A-Z0-9]+" # 测试提取 tryCatch({ matches <- gregexpr(regex_pattern, example_text, perl = TRUE) extracted <- regmatches(example_text, matches) # 提取所有匹配项 all_matches <- unique(unlist(extracted)) all_matches <- all_matches[all_matches != ""] if (length(all_matches) == 0) { div( class = "alert alert-warning", h6("⚠️ 未找到匹配"), p("当前正则表达式无法从示例文本中提取基因符号。"), p(sprintf("正则表达式: %s", regex_pattern)) ) } else { tagList( div( class = "alert alert-success", h6("✅ 提取成功", style = "color: #28a745;"), p(sprintf("找到 %d 个匹配:", length(all_matches))), div( style = "background: #f8f9fa; padding: 10px; border-radius: 5px; margin-top: 10px;", tags$ul(style = "margin: 0; padding-left: 20px;", tagList(lapply(all_matches, function(m) { tags$li(style = "margin: 5px 0; font-family: monospace; color: #667eea;", sprintf("%s", m)) })) ) ) ), div( style = "margin-top: 10px;", h6("📊 提取统计:", style = "color: #667eea;"), tags$table( class = "table table-striped", tags$tbody( tags$tr( tags$td("原始文本长度"), tags$td(nchar(example_text)) ), tags$tr( tags$td("提取数量"), tags$td(length(all_matches)) ), tags$tr( tags$td("平均长度"), tags$td(round(mean(nchar(all_matches)), 1)) ) ) ) ) ) } }, error = function(e) { div( class = "alert alert-danger", h6("❌ 正则表达式错误"), p(e$message) ) }) }) # 正则预设按钮逻辑 observeEvent(input$chip_regex_preset1, { updateTextInput(session, "chip_gene_regex", value = "[A-Z]+") }) observeEvent(input$chip_regex_preset2, { updateTextInput(session, "chip_gene_regex", value = "[A-Z][A-Z0-9]+") }) observeEvent(input$chip_regex_preset3, { updateTextInput(session, "chip_gene_regex", value("\\(([^)]+)\\)")) }) # 步骤5: 预览合并结果 observeEvent(input$chip_preview_merge, { req(chip_data$series_matrix) req(chip_data$soft_platform) req(input$chip_soft_id_col) req(input$chip_soft_gene_col) # 转换Series Matrix行名为列 series_df <- as.data.frame(chip_data$series_matrix) if (input$chip_convert_rownames %||% TRUE) { series_df <- data.frame(ProbeID = rownames(series_df), series_df, row.names = NULL) } # 准备SOFT数据 soft_df <- chip_data$soft_platform[, c(input$chip_soft_id_col, input$chip_soft_gene_col)] colnames(soft_df) <- c("ID", "GeneSymbol") # 合并数据 - 简化版预览 merged_df <- merge(series_df, soft_df, by.x = "ProbeID", by.y = "ID", all.x = TRUE) # 重新排序列:ProbeID | GeneSymbol | 样本列 sample_cols <- colnames(merged_df)[!colnames(merged_df) %in% c("ProbeID", "GeneSymbol", "ID")] merged_df <- merged_df[, c("ProbeID", "GeneSymbol", sample_cols)] # 保存预览结果 chip_data$merged_preview <- head(merged_df, 5) showNotification("✅ 合并预览已生成", type = "message") }) # 显示合并预览表格 output$chip_merge_preview_table <- renderDT({ req(chip_data$merged_preview) datatable( chip_data$merged_preview, options = list( dom = 't', paging = FALSE, scrollX = TRUE ), rownames = FALSE, filter = 'none' ) %>% formatStyle(columns = c("ProbeID", "Gene"), fontSize = '90%') }) # 步骤5: 应用合并配置 observeEvent(input$chip_apply_merge, { req(chip_data$series_matrix) req(chip_data$soft_platform) req(input$chip_soft_id_col) req(input$chip_soft_gene_col) showNotification("🔄 正在应用配置并生成最终矩阵...", type = "message") # 转换Series Matrix行名为列 series_df <- as.data.frame(chip_data$series_matrix) if (input$chip_convert_rownames %||% TRUE) { series_df <- data.frame(ProbeID = rownames(series_df), series_df, row.names = NULL) } cat(sprintf("🔍 Series探针ID示例: %s\n", paste(head(series_df$ProbeID, 3), collapse = ", "))) # 🔍 智能检测SOFT文件中的探针ID列 user_id_col <- input$chip_soft_id_col user_id_sample <- head(chip_data$soft_platform[[user_id_col]][!is.na(chip_data$soft_platform[[user_id_col]])], 10) cat(sprintf("🔍 用户选择的ID列 '%s' 示例: %s\n", user_id_col, paste(head(user_id_sample, 3), collapse = ", "))) # 检查用户选择的ID列是否真的包含探针ID # 支持多种探针ID格式: # 1. Affymetrix: 12345_at, 1234567_x_at, 1234567_at # 2. Agilent: A_23_P100001, CN_123456 # 3. Illumina: ILMN_123456 is_probe_format <- any(grepl(".*_.*_at$|.*_at$|at$", user_id_sample)) || # Affymetrix any(grepl("^[A-Z]+_\\d+_", user_id_sample)) || # Agilent/Illumina前缀 any(grepl("^CN_", user_id_sample)) # 其他常见格式 # 准备SOFT数据 if (is_probe_format) { cat("✅ 用户选择的ID列包含探针格式\n") soft_df <- chip_data$soft_platform[, c(user_id_col, input$chip_soft_gene_col)] colnames(soft_df) <- c("ID", "Gene_Raw") } else { cat("⚠️ 用户选择的ID列不包含探针格式,尝试使用SOFT行名\n") # 尝试使用SOFT文件的行名作为探针ID if (!is.null(rownames(chip_data$soft_platform))) { # 检查行名是否匹配探针ID rowname_sample <- head(rownames(chip_data$soft_platform), 10) cat(sprintf("🔍 SOFT行名示例: %s\n", paste(rowname_sample, collapse = ", "))) # 创建临时数据框使用行名 soft_df <- data.frame( ID = rownames(chip_data$soft_platform), Gene_Raw = chip_data$soft_platform[[input$chip_soft_gene_col]], stringsAsFactors = FALSE ) } else { cat("❌ SOFT文件没有行名,无法自动检测\n") showNotification("⚠️ 请选择包含探针ID的列(如ID、SPOT_ID等)", type = "warning", duration = 10) return() } } # 🆕 智能判断是否需要正则提取 # 检查基因列的内容,如果已经是纯符号格式,不需要提取 gene_sample <- head(soft_df$Gene_Raw[!is.na(soft_df$Gene_Raw) & soft_df$Gene_Raw != ""], 10) cat(sprintf("🔍 用户选择的基因列 '%s' 示例: %s\n", input$chip_soft_gene_col, paste(head(gene_sample, 3), collapse = ", "))) # 判断是否是纯数字ID is_numeric_id <- all(grepl("^[0-9]+$", gene_sample)) if (is_numeric_id) { cat("⚠️ 用户选择的基因列包含数字ID而非基因符号!\n") cat("💡 建议:请选择包含基因符号的列(如GENE_SYMBOL)\n") # 尝试自动查找真正的基因符号列 soft_cols <- colnames(chip_data$soft_platform) cat(sprintf("🔍 SOFT文件所有列名: %s\n", paste(soft_cols, collapse = ", "))) possible_symbol_cols <- c("GENE_SYMBOL", "SYMBOL", "GENE_NAME", "NAME", "DESCRIPTION") for (col in possible_symbol_cols) { if (col %in% soft_cols && col != input$chip_soft_gene_col) { test_data <- head(chip_data$soft_platform[[col]][!is.na(chip_data$soft_platform[[col]])], 10) cat(sprintf("🔍 检查列 '%s': 示例=%s\n", col, paste(head(test_data, 3), collapse = ", "))) if (!all(grepl("^[0-9]+$", test_data))) { cat(sprintf("✅ 自动检测到基因符号列: %s\n", col)) cat(sprintf(" 示例: %s\n", paste(head(test_data, 3), collapse = ", "))) # 🔧 修复:创建新的soft_df,直接包含正确的列 soft_df <- chip_data$soft_platform[, c(user_id_col, col)] colnames(soft_df) <- c("ID", "Gene_Raw") cat("✅ 已重新创建soft_df,使用正确的基因符号列\n") cat(sprintf("✅ 验证:soft_df的Gene_Raw列示例: %s\n", paste(head(soft_df$Gene_Raw[!is.na(soft_df$Gene_Raw)], 3), collapse = ", "))) break } } } } # 重新检查Gene_Raw gene_sample <- head(soft_df$Gene_Raw[!is.na(soft_df$Gene_Raw) & soft_df$Gene_Raw != ""], 10) # 判断是否已经是纯基因符号(不包含额外文本) # 典型基因符号:TP53, EGFR, BRCA1, MYC (大写字母+数字,无空格,无逗号) is_pure_symbol <- all(grepl("^[A-Z][A-Z0-9]{1,15}$", gene_sample)) if (is_pure_symbol) { cat("✅ 基因列已是纯符号格式,直接使用,无需正则提取\n") cat(sprintf(" 示例: %s\n", paste(head(gene_sample, 3), collapse = ", "))) # 直接使用原列作为基因符号 soft_df$GeneSymbol <- soft_df$Gene_Raw cat(sprintf("✅ GeneSymbol列已创建,示例: %s\n", paste(head(soft_df$GeneSymbol[!is.na(soft_df$GeneSymbol)], 3), collapse = ", "))) } else { cat("📋 基因列包含额外文本,应用正则提取\n") cat(sprintf(" 原始示例: %s\n", paste(head(gene_sample, 2), collapse = ", "))) # 应用正则提取 regex_pattern <- input$chip_gene_regex %||% "[A-Z][A-Z0-9]+" soft_df$GeneSymbol <- sapply(soft_df$Gene_Raw, function(x) { matches <- regmatches(x, gregexpr(regex_pattern, as.character(x), perl = TRUE)) if (length(matches[[1]]) > 0) { matches[[1]][1] # 取第一个匹配 } else { NA } }) extracted_sample <- head(soft_df$GeneSymbol[!is.na(soft_df$GeneSymbol)], 5) cat(sprintf(" 提取示例: %s\n", paste(extracted_sample, collapse = ", "))) } # 🔧 尝试获取Entrez Gene ID(支持用户指定或自动检测) # 🔧 通用函数:清理EntrezID(移除非数字字符) clean_entrez_id <- function(entrez_str) { if (is.na(entrez_str) || is.null(entrez_str) || entrez_str == "") { return(NA) } # 转换为字符 entrez_str <- as.character(entrez_str) # 移除所有非数字字符(保留数字0-9) cleaned <- gsub("[^0-9]", "", entrez_str) # 如果清理后为空,返回NA if (cleaned == "" || is.na(cleaned)) { return(NA) } return(cleaned) } # 方法1: 用户手动指定EntrezID列 if (!is.null(input$chip_soft_entrez_col) && input$chip_soft_entrez_col != "" && input$chip_soft_entrez_col != "自动检测") { user_entrez_col <- input$chip_soft_entrez_col if (user_entrez_col %in% colnames(chip_data$soft_platform)) { raw_entrez_ids <- as.character(chip_data$soft_platform[[user_entrez_col]][match(soft_df$ID, chip_data$soft_platform[[input$chip_soft_id_col]])]) # 🔧 清理EntrezID(移除非数字字符) entrez_gene_ids <- sapply(raw_entrez_ids, clean_entrez_id) soft_df$EntrezID <- entrez_gene_ids # 统计清理情况 na_count <- sum(is.na(entrez_gene_ids)) valid_count <- sum(!is.na(entrez_gene_ids)) entrez_sample <- head(entrez_gene_ids[!is.na(entrez_gene_ids)], 3) cat(sprintf("✅ 用户指定的EntrezID列 '%s' 已添加\n", user_entrez_col)) cat(sprintf(" 有效ID数: %d, NA数: %d\n", valid_count, na_count)) cat(sprintf(" 示例: %s\n", paste(entrez_sample, collapse = ", "))) if (na_count > 0) { cat(sprintf(" ⚠️ %d个ID被清理为NA(包含非数字字符)\n", na_count)) } } } # 方法2: 自动检测EntrezID列(如果用户选择"自动检测") else if (!is.null(input$chip_soft_entrez_col) && input$chip_soft_entrez_col == "自动检测") { # 常见的EntrezID列名(按优先级排序) possible_entrez_cols <- c("ENTREZ_GENE_ID", "ENTREZID", "EntrezID", "GeneID", "GENE_ID", "ENTREZ_GENE", "GENE", "Entrez Gene ID", "Entrez", "ENTREZ") found_entrez_col <- NULL for (col in possible_entrez_cols) { if (col %in% colnames(chip_data$soft_platform)) { # 检查该列是否包含数字ID(清理后) test_values <- head(chip_data$soft_platform[[col]], 100) test_values <- test_values[!is.na(test_values) & test_values != ""] # 🔧 清理测试值(移除非数字字符) cleaned_test <- sapply(test_values, clean_entrez_id) cleaned_test <- cleaned_test[!is.na(cleaned_test)] # 检查是否大部分可以清理为纯数字(EntrezID特征) if (length(cleaned_test) > 0) { is_numeric_id <- sum(grepl("^[0-9]+$", cleaned_test)) / length(cleaned_test) > 0.8 if (is_numeric_id) { found_entrez_col <- col cat(sprintf("✅ 自动检测到EntrezID列: '%s'\n", col)) break } } } } if (!is.null(found_entrez_col)) { raw_entrez_ids <- as.character(chip_data$soft_platform[[found_entrez_col]][match(soft_df$ID, chip_data$soft_platform[[input$chip_soft_id_col]])]) # 🔧 清理EntrezID(移除非数字字符) entrez_gene_ids <- sapply(raw_entrez_ids, clean_entrez_id) soft_df$EntrezID <- entrez_gene_ids # 统计清理情况 na_count <- sum(is.na(entrez_gene_ids)) valid_count <- sum(!is.na(entrez_gene_ids)) entrez_sample <- head(entrez_gene_ids[!is.na(entrez_gene_ids)], 3) cat(sprintf("✅ 自动检测的EntrezID列已添加\n", found_entrez_col)) cat(sprintf(" 有效ID数: %d, NA数: %d\n", valid_count, na_count)) cat(sprintf(" 示例: %s\n", paste(entrez_sample, collapse = ", "))) if (na_count > 0) { cat(sprintf(" ⚠️ %d个ID被清理为NA(包含非数字字符)\n", na_count)) } } else { cat("⚠️ 自动检测未找到EntrezID列\n") } } # 方法3: 旧逻辑(仅检查GENE列)- 也添加清理 else if ("GENE" %in% colnames(chip_data$soft_platform)) { raw_entrez_ids <- as.character(chip_data$soft_platform$GENE[match(soft_df$ID, chip_data$soft_platform[[input$chip_soft_id_col]])]) # 🔧 清理EntrezID entrez_gene_ids <- sapply(raw_entrez_ids, clean_entrez_id) soft_df$EntrezID <- entrez_gene_ids # 统计清理情况 na_count <- sum(is.na(entrez_gene_ids)) valid_count <- sum(!is.na(entrez_gene_ids)) entrez_sample <- head(entrez_gene_ids[!is.na(entrez_gene_ids)], 3) cat(sprintf("✅ Entrez Gene ID列(GENE)已添加\n")) cat(sprintf(" 有效ID数: %d, NA数: %d\n", valid_count, na_count)) cat(sprintf(" 示例: %s\n", paste(entrez_sample, collapse = ", "))) if (na_count > 0) { cat(sprintf(" ⚠️ %d个ID被清理为NA(包含非数字字符)\n", na_count)) } } # 🔧 不再创建Gene列!直接使用GeneSymbol用于去重,EntrezID用于差异分析 cat("✅ 使用GeneSymbol列进行探针去重\n") cat("✅ 使用EntrezID列作为差异分析结果的ID\n") # 合并数据 - 保留所有列 merged_df <- merge(series_df, soft_df, by.x = "ProbeID", by.y = "ID", all.x = TRUE) # 🔧 智能识别样本列(必须是数值型,排除ID列和基因列) # 规则:样本列 = 数值型列 + 不在排除列表中 exclude_cols <- c("ProbeID", "GeneSymbol", "EntrezID", "Gene_Raw", "ID") # 识别数值型列(样本列) sample_cols <- character(0) for (col in colnames(merged_df)) { if (!(col %in% exclude_cols)) { # 检查是否为数值型 if (is.numeric(merged_df[[col]])) { sample_cols <- c(sample_cols, col) } } } cat(sprintf("🔍 识别到 %d 个样本列: %s\n", length(sample_cols), paste(head(sample_cols, 5), collapse = ", "))) # 重新排序列:ProbeID | GeneSymbol | EntrezID | 样本列 if ("EntrezID" %in% colnames(merged_df)) { # 重新排列列顺序 merged_df <- merged_df[, c("ProbeID", "GeneSymbol", "EntrezID", sample_cols)] # 🔧 强制确保EntrezID是字符型 merged_df$EntrezID <- as.character(merged_df$EntrezID) cat("✅ EntrezID列已强制转换为字符型\n") } else { # 如果没有EntrezID,只保留GeneSymbol merged_df <- merged_df[, c("ProbeID", "GeneSymbol", sample_cols)] cat("⚠️ 未找到EntrezID列\n") } # 🔍 诊断:显示合并后的列结构 cat(sprintf("🔍 合并后矩阵结构: %d 行 × %d 列\n", nrow(merged_df), ncol(merged_df))) cat(sprintf("🔍 列名: %s\n", paste(colnames(merged_df), collapse = ", "))) cat(sprintf("🔍 前3列: %s\n", paste(head(colnames(merged_df), 3), collapse = ", "))) cat(sprintf("🔍 后3列: %s\n", paste(tail(colnames(merged_df), 3), collapse = ", "))) # 显示各列的类型 cat("🔍 列类型:\n") for (i in 1:min(5, ncol(merged_df))) { cat(sprintf(" %s: %s\n", colnames(merged_df)[i], class(merged_df[[i]]))) } # 确认EntrezID列的位置和类型 if ("EntrezID" %in% colnames(merged_df)) { entrez_col_idx <- which(colnames(merged_df) == "EntrezID") cat(sprintf("🔍 EntrezID列位置: 第%d列,类型: %s\n", entrez_col_idx, class(merged_df$EntrezID))) cat(sprintf("🔍 EntrezID示例: %s\n", paste(head(merged_df$EntrezID[!is.na(merged_df$EntrezID)], 3), collapse = ", "))) } # 保存最终合并的矩阵 chip_data$merged_matrix <- merged_df # 🆕 检查数据列内容 gene_symbol_sample <- head(merged_df$GeneSymbol[!is.na(merged_df$GeneSymbol)], 5) cat(sprintf("✅ GeneSymbol列包含基因符号(示例: %s)\n", paste(gene_symbol_sample, 3), collapse = ", ")) if ("EntrezID" %in% colnames(merged_df)) { entrez_sample <- head(merged_df$EntrezID[!is.na(merged_df$EntrezID)], 5) cat(sprintf("✅ EntrezID列包含Entrez Gene ID(示例: %s)\n", paste(entrez_sample, collapse = ", "))) } # 统计信息 n_total <- nrow(merged_df) n_annotated <- sum(!is.na(merged_df$GeneSymbol)) annotation_rate <- n_annotated / n_total * 100 # 🔍 诊断信息:检查GeneSymbol列匹配情况 na_count <- sum(is.na(merged_df$GeneSymbol) | merged_df$GeneSymbol == "") cat(sprintf("🔍 诊断: %d个探针的GeneSymbol为NA或空 (%.1f%%)\n", na_count, na_count/n_total*100)) # 检查ID匹配情况 series_probes <- series_df$ProbeID soft_ids <- soft_df$ID matched_probes <- sum(series_probes %in% soft_ids) cat(sprintf("🔍 ID匹配: %d / %d 个Series探针在SOFT文件中找到 (%.1f%%)\n", matched_probes, length(series_probes), matched_probes/length(series_probes)*100)) # 如果匹配率太低,显示警告 if (matched_probes / length(series_probes) < 0.5) { cat("⚠️ 警告:ID匹配率低于50%,请检查ID列选择是否正确!\n") cat(sprintf(" Series探针示例: %s\n", paste(head(series_probes, 3), collapse = ", "))) cat(sprintf(" SOFT ID示例: %s\n", paste(head(soft_ids, 3), collapse = ", "))) } cat(sprintf("✅ 合并完成: %d 个探针, %d 个已注释 (%.1f%%)\n", n_total, n_annotated, annotation_rate)) showNotification( sprintf("✅ 合并完成!%d / %d 个探针已注释 (%.1f%%)", n_annotated, n_total, annotation_rate), type = "message", duration = 10 ) }) # 显示最终合并矩阵的预览(在应用合并后) output$chip_final_matrix_ui <- renderUI({ req(chip_data$merged_matrix) tagList( h5("📊 最终表达矩阵(前5行)", style = "color: #28a745;"), DTOutput("chip_final_matrix_table") ) }) output$chip_final_matrix_table <- renderDT({ req(chip_data$merged_matrix) # 显示前5行和前10列(避免表格过宽) preview_df <- head(chip_data$merged_matrix, 5) if (ncol(preview_df) > 12) { preview_df <- preview_df[, 1:12] # ProbeID + Gene + 前10个样本 } # 检查是否有ProbeID和Gene列 has_probe_id <- "ProbeID" %in% colnames(preview_df) has_gene <- "Gene" %in% colnames(preview_df) # 创建datatable dt <- datatable( preview_df, options = list( dom = 't', paging = FALSE, scrollX = TRUE, columnDefs = list(list( className = 'dt-center', targets = "_all" )) ), rownames = FALSE, filter = 'none' ) # 只对存在的列应用样式 if (has_probe_id && has_gene) { dt <- dt %>% formatStyle(columns = c("ProbeID", "Gene"), backgroundColor = '#e8f4f8', fontWeight = 'bold') } dt }) # ============================================ # 🆕 数据预处理与探针去重 - Server逻辑 # ============================================ # 去重前统计显示 output$chip_before_dedupe_stats <- renderUI({ req(chip_data$merged_matrix) # 统计探针数量 n_probes <- nrow(chip_data$merged_matrix) # 识别数值列(样本数据) numeric_cols <- sapply(chip_data$merged_matrix, function(x) is.numeric(x)) n_samples <- sum(numeric_cols) # 统计探针-基因映射情况 n_with_gene <- sum(!is.na(chip_data$merged_matrix$GeneSymbol)) # 计算一个基因对应多个探针的情况 gene_counts <- table(chip_data$merged_matrix$GeneSymbol) gene_counts <- gene_counts[names(gene_counts) != ""] # 移除空基因名 n_multi_probes <- sum(gene_counts > 1) div( style = "font-size: 12px;", tags$ul(style = "padding-left: 15px; margin: 5px 0;", tags$li(sprintf("总探针数: %d", n_probes)), tags$li(sprintf("样本数: %d", n_samples)), tags$li(sprintf("有基因注释的探针: %d (%.1f%%)", n_with_gene, n_with_gene/n_probes*100)), tags$li(sprintf("一因多探针的基因数: %d", n_multi_probes)) ) ) }) # 步骤1: 数据预处理 observeEvent(input$chip_preprocess_data, { req(chip_data$merged_matrix) showNotification("🔄 正在进行数据预处理...", type = "message") tryCatch({ # 🔧 修复:智能提取表达数据(只保留数值列) merged_df <- chip_data$merged_matrix # 识别数值列 numeric_cols <- sapply(merged_df, function(x) is.numeric(x)) # 排除ProbeID和Gene列(它们在前两列) # 但要确保只使用数值列进行计算 expr_cols <- which(numeric_cols) # 提取表达数据 expr_matrix <- as.matrix(merged_df[, expr_cols, drop = FALSE]) # 🔧 修复:使用ProbeID作为行名,而不是Gene(这样才能在去重时正确匹配) if ("ProbeID" %in% colnames(merged_df)) { rownames(expr_matrix) <- merged_df$ProbeID cat("✅ 表达矩阵行名使用ProbeID\n") } else if ("Gene" %in% colnames(merged_df)) { rownames(expr_matrix) <- merged_df$Gene cat("⚠️ 警告:使用Gene作为行名(ProbeID列不存在)\n") } else { rownames(expr_matrix) <- rownames(merged_df) } cat(sprintf("✅ 提取表达数据: %d 探针 × %d 样本\n", nrow(expr_matrix), ncol(expr_matrix))) # 保存原始数据用于对比 chip_data$expr_before_preprocess <- expr_matrix # 1. 自动log2转换判断 if (input$chip_auto_log2 %||% TRUE) { ex <- expr_matrix qx <- as.numeric(quantile(ex, c(0., 0.25, 0.5, 0.75, 0.99, 1.0), na.rm = TRUE)) LogC <- (qx[5] > 100) || (qx[6] - qx[1] > 50 && qx[2] > 0) || (qx[2] > 0 && qx[2] < 1 && qx[4] > 1 && qx[4] < 2) if (LogC) { ex[which(ex <= 0)] <- NaN expr_matrix <- log2(ex) chip_data$log2_performed <- TRUE cat("✅ log2转换已完成\n") } else { chip_data$log2_performed <- FALSE cat("ℹ️ 不需要log2转换\n") } } # 2. limma标准化 if (input$chip_normalize_data %||% TRUE) { library(limma) expr_matrix <- normalizeBetweenArrays(expr_matrix) chip_data$normalize_performed <- TRUE cat("✅ limma标准化已完成\n") } # 保存处理后的数据 chip_data$expr_preprocessed <- expr_matrix # 生成结果报告 chip_data$preprocess_report <- list( log2_performed = chip_data$log2_performed, normalize_performed = chip_data$normalize_performed, n_probes = nrow(expr_matrix), n_samples = ncol(expr_matrix), data_range = range(expr_matrix, na.rm = TRUE) ) # 🆕 生成箱线图对比(矫正前后) tryCatch({ library(ggplot2) library(reshape2) # 矫正前的数据 - 计算每个样本的统计值用于箱线图 expr_before <- chip_data$expr_before_preprocess # 转置矩阵:行为样本,列为探针 df_before <- as.data.frame(t(expr_before)) df_before$Sample <- rownames(df_before) # 将数据从宽格式转换为长格式(使用reshape2的melt) df_before_long <- melt(df_before, id.vars = "Sample", variable.name = "Probe", value.name = "Expression") df_before_long$Stage <- "Before" # 矫正后的数据 df_after <- as.data.frame(t(expr_matrix)) df_after$Sample <- rownames(df_after) df_after_long <- melt(df_after, id.vars = "Sample", variable.name = "Probe", value.name = "Expression") df_after_long$Stage <- "After" # 合并数据 df_combined <- rbind(df_before_long, df_after_long) # 绘制箱线图 p <- ggplot(df_combined, aes(x = Sample, y = Expression, fill = Stage)) + geom_boxplot() + theme_bw() + theme( axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5, size = 8), legend.position = "top", plot.title = element_text(hjust = 0.5) ) + labs( title = "数据预处理前后对比", x = "样本", y = "表达值", fill = "阶段" ) + scale_fill_manual(values = c("Before" = "#E69F00", "After" = "#009E73")) chip_data$preprocess_boxplot <- p cat("✅ 箱线图已生成\n") }, error = function(e) { cat(sprintf("⚠️ 箱线图生成失败: %s\n", e$message)) }) showNotification("✅ 数据预处理完成!", type = "message", duration = 5) }, error = function(e) { showNotification(sprintf("❌ 预处理失败: %s", e$message), type = "error") }) }) # 预处理结果显示 output$chip_preprocess_result_ui <- renderUI({ req(chip_data$preprocess_report) req(chip_data$expr_preprocessed) report <- chip_data$preprocess_report expr_matrix <- chip_data$expr_preprocessed # 计算额外统计信息 expr_mean <- mean(expr_matrix, na.rm = TRUE) expr_median <- median(expr_matrix, na.rm = TRUE) expr_sd <- sd(expr_matrix, na.rm = TRUE) tagList( h5("✅ 预处理完成", style = "color: #28a745; font-weight: bold;"), br(), # 基本统计信息 wellPanel( style = "background: #f8f9fa; border: 1px solid #dee2e6;", h6("📊 基本统计", style = "color: #495057; margin-top: 0;"), tags$table( class = "table table-sm table-striped", style = "margin-bottom: 0;", tags$thead( tags$tr( tags$th("项目", style = "width: 50%;"), tags$th("值") ) ), tags$tbody( tags$tr( tags$td("log2转换"), tags$td({ if (report$log2_performed) tags$span("✅ 是", class = "badge badge-success") else tags$span("❌ 否", class = "badge badge-secondary") }) ), tags$tr( tags$td("limma标准化 (quantile)"), tags$td({ if (report$normalize_performed) tags$span("✅ 是", class = "badge badge-success") else tags$span("❌ 否", class = "badge badge-secondary") }) ), tags$tr( tags$td(tags$strong("探针数")), tags$td(sprintf("%d", report$n_probes)) ), tags$tr( tags$td(tags$strong("样本数")), tags$td(sprintf("%d", report$n_samples)) ), tags$tr( tags$td("数据范围"), tags$td(sprintf("%.3f ~ %.3f", report$data_range[1], report$data_range[2])) ), tags$tr( tags$td("平均值"), tags$td(sprintf("%.3f", expr_mean)) ), tags$tr( tags$td("中位数"), tags$td(sprintf("%.3f", expr_median)) ), tags$tr( tags$td("标准差"), tags$td(sprintf("%.3f", expr_sd)) ) ) ) ), br(), # 箱线图对比 wellPanel( style = "background: white; border: 1px solid #dee2e6;", h6("📊 箱线图对比(矫正前后)", style = "color: #ff9800; margin-top: 0;"), plotOutput("chip_preprocess_boxplot", height = "500px") ) ) }) # 🆕 渲染箱线图 output$chip_preprocess_boxplot <- renderPlot({ req(chip_data$preprocess_boxplot) chip_data$preprocess_boxplot }) # ============================================ # 🆕 批次矫正 - Server逻辑 # ============================================ # 自动检测批次 observeEvent(input$chip_detect_batch, { req(chip_data$expr_preprocessed) req(input$chip_batch_pattern) pattern <- input$chip_batch_pattern sample_names <- colnames(chip_data$expr_preprocessed) # 从样本名中提取批次信息 batches <- sapply(sample_names, function(x) { match <- regmatches(x, regexpr(paste0(pattern, "\\w+"), x, ignore.case = TRUE)) if (length(match) > 0) { return(match) } else { return("Unknown") } }) # 保存批次信息 chip_data$batch_info <- batches chip_data$batch_method <- input$chip_batch_method %||% "limma" # 统计批次分布 batch_table <- table(batches) cat(sprintf("✅ 检测到 %d 个批次:\n", length(batch_table))) for (i in seq_along(batch_table)) { cat(sprintf(" - %s: %d 个样本\n", names(batch_table)[i], batch_table[i])) } showNotification( sprintf("✅ 检测到 %d 个批次", length(batch_table)), type = "message" ) }) # 解析手动指定的批次 observeEvent(input$chip_batch_manual, { req(input$chip_batch_manual) tryCatch({ lines <- strsplit(input$chip_batch_manual, "\n")[[1]] lines <- lines[lines != "" & !grepl("^\\s*$", lines)] batch_mapping <- list() for (line in lines) { parts <- strsplit(line, "\t")[[1]] if (length(parts) >= 2) { batch_mapping[[trimws(parts[1])]] <- trimws(parts[2]) } } chip_data$batch_manual_mapping <- batch_mapping sample_names <- colnames(chip_data$expr_preprocessed) batches <- sapply(sample_names, function(x) { if (x %in% names(batch_mapping)) { return(batch_mapping[[x]]) } else { return("Unknown") } }) chip_data$batch_info <- batches batch_table <- table(batches) cat(sprintf("✅ 手动指定批次: %d 个批次\n", length(batch_table))) showNotification( sprintf("✅ 手动指定批次: %d 个批次", length(batch_table)), type = "message" ) }, error = function(e) { showNotification(sprintf("❌ 解析批次信息失败: %s", e$message), type = "error") }) }) # 显示批次信息 output$chip_batch_info_ui <- renderUI({ req(chip_data$batch_info) batches <- chip_data$batch_info batch_table <- table(batches) tagList( tags$table( class = "table table-striped", tags$thead( tags$tr( tags$th("批次"), tags$th("样本数") ) ), tags$tbody( lapply(seq_along(batch_table), function(i) { tags$tr( tags$td(names(batch_table)[i]), tags$td(batch_table[i]) ) }) ) ) ) }) # 执行批次矫正 observeEvent(input$chip_apply_batch_correct, { req(chip_data$expr_preprocessed) req(chip_data$batch_info) showNotification("🔧 正在进行批次矫正...", type = "message") tryCatch({ expr_matrix <- chip_data$expr_preprocessed batches <- chip_data$batch_info method <- input$chip_batch_method %||% "limma" cat(sprintf("📊 批次矫正方法: %s\n", method)) if (method == "limma") { # 使用 limma::removeBatchEffect library(limma) batch_factor <- factor(batches) design <- model.matrix(~1, data = data.frame(batch = batch_factor)) expr_corrected <- removeBatchEffect(expr_matrix, batch = batch_factor) cat("✅ limma::removeBatchEffect 批次矫正完成\n") } else if (method == "combat") { # 使用 sva::ComBat library(sva) batch_factor <- factor(batches) expr_corrected <- ComBat( dat = expr_matrix, batch = batch_factor, mod = NULL, par.prior = TRUE, prior.plots = FALSE ) cat("✅ sva::ComBat 批次矫正完成\n") } # 保存矫正后的数据 chip_data$expr_batch_corrected <- expr_corrected chip_data$batch_correct_method <- method # 计算矫正前后的差异 mean_diff <- mean(abs(expr_matrix - expr_corrected), na.rm = TRUE) # 生成报告 chip_data$batch_correct_report <- list( method = method, n_batches = length(unique(batches)), batch_distribution = table(batches), mean_change = mean_diff, n_probes = nrow(expr_corrected), n_samples = ncol(expr_corrected) ) showNotification( sprintf("✅ 批次矫正完成!方法: %s", method), type = "message", duration = 10 ) }, error = function(e) { showNotification( sprintf("❌ 批次矫正失败: %s", e$message), type = "error", duration = 10 ) cat(sprintf("❌ 批次矫正错误: %s\n", e$message)) }) }) # 显示批次矫正结果 output$chip_batch_correct_result_ui <- renderUI({ req(chip_data$batch_correct_report) report <- chip_data$batch_correct_report method_name <- if (report$method == "limma") { "limma::removeBatchEffect" } else { "sva::ComBat" } tagList( h6("✅ 批次矫正完成", style = "color: #E91E63;"), tags$table( class = "table table-striped", tags$thead( tags$tr( tags$th("项目"), tags$th("值") ) ), tags$tbody( tags$tr( tags$td("矫正方法"), tags$td(method_name) ), tags$tr( tags$td("批次数"), tags$td(report$n_batches) ), tags$tr( tags$td("探针/基因数"), tags$td(report$n_probes) ), tags$tr( tags$td("样本数"), tags$td(report$n_samples) ), tags$tr( tags$td("平均变化幅度"), tags$td(sprintf("%.4f", report$mean_change)) ) ) ), br(), helpText("💡 提示:批次矫正后的数据已保存,将用于后续的探针去重和差异分析。") ) }) # 步骤2: 探针去重 observeEvent(input$chip_dedupe_probes, { req(chip_data$expr_preprocessed) showNotification("✂️ 正在进行探针去重...", type = "message") tryCatch({ library(dplyr) library(tibble) # ✅ 优先使用批次矫正后的数据 if (!is.null(chip_data$expr_batch_corrected)) { expr_matrix <- chip_data$expr_batch_corrected cat("✅ 使用批次矫正后的数据进行探针去重\n") } else { expr_matrix <- chip_data$expr_preprocessed cat("✅ 使用预处理后的数据进行探针去重\n") } # 转换为数据框并添加探针ID和基因信息 expr_df <- as.data.frame(expr_matrix) expr_df <- expr_df %>% rownames_to_column("ProbeID") # 🔧 检查merged_matrix中的可用列 available_cols <- colnames(chip_data$merged_matrix) cat(sprintf("📋 merged_matrix可用列: %s\n", paste(available_cols, collapse = ", "))) # 动态选择要合并的列(只选择存在的列) merge_cols <- c("ProbeID") if ("GeneSymbol" %in% available_cols) { merge_cols <- c(merge_cols, "GeneSymbol") } if ("EntrezID" %in% available_cols) { merge_cols <- c(merge_cols, "EntrezID") } cat(sprintf("📋 将合并列: %s\n", paste(merge_cols, collapse = ", "))) # 执行合并 expr_df <- expr_df %>% inner_join(chip_data$merged_matrix[, merge_cols, drop = FALSE], by = "ProbeID") # 🔧 清理合并后的EntrezID列(移除非数字字符) if ("EntrezID" %in% colnames(expr_df)) { clean_entrez_id <- function(entrez_str) { if (is.na(entrez_str) || is.null(entrez_str) || entrez_str == "") { return(NA) } entrez_str <- as.character(entrez_str) # 移除所有非数字字符(///, //, -, 等) cleaned <- gsub("[^0-9]", "", entrez_str) if (cleaned == "" || is.na(cleaned)) { return(NA) } return(cleaned) } # 统计清理前的情况 na_before <- sum(is.na(expr_df$EntrezID)) # 应用清理 expr_df$EntrezID <- sapply(expr_df$EntrezID, clean_entrez_id) # 统计清理后的情况 na_after <- sum(is.na(expr_df$EntrezID)) cat(sprintf("🔧 EntrezID清理: NA前=%d, NA后=%d, 新增NA=%d\n", na_before, na_after, na_after - na_before)) if (na_after > na_before) { cat(sprintf("⚠️ %d个EntrezID包含非数字字符,已清理为NA\n", na_after - na_before)) } } # 🔍 诊断:检查合并后的GeneSymbol列 cat(sprintf("🔍 去重前: %d 行,GeneSymbol NA=%d, 空字符串=%d\n", nrow(expr_df), sum(is.na(expr_df$GeneSymbol)), sum(expr_df$GeneSymbol == ""))) # 移除没有基因符号的探针 expr_df <- expr_df[!is.na(expr_df$GeneSymbol) & expr_df$GeneSymbol != "", ] cat(sprintf("🔍 移除NA后: %d 行剩余\n", nrow(expr_df))) # 如果移除后没有数据,警告并跳过去重 if (nrow(expr_df) == 0) { cat("⚠️ 警告:移除NA基因后没有数据!请检查合并步骤的GeneSymbol列\n") showNotification("❌ 去重失败:所有基因都是NA,请检查ID列和基因列是否匹配", type = "error", duration = 10) return() } # 探针去重:保留表达量最高的探针 # 使用GeneSymbol去重,但保留EntrezID列供差异分析使用 # 最终expr_deduped包含:行名=GeneSymbol, 列=EntrezID + 样本数据 if ("EntrezID" %in% colnames(expr_df)) { expr_df <- expr_df %>% select(-ProbeID) %>% # 去掉探针ID列 select(GeneSymbol, EntrezID, everything()) %>% # 基因符号和ID放前面 mutate(rowMean = rowMeans(.[, -(1:2)])) %>% # 计算平均表达量(排除前两列) arrange(desc(rowMean)) %>% # 按表达量降序排列 distinct(GeneSymbol, .keep_all = TRUE) %>% # 按基因符号去重 select(-rowMean) %>% # 删除辅助列(保留EntrezID列!) column_to_rownames("GeneSymbol") # GeneSymbol作为行名,EntrezID保留为列 cat("✅ 去重使用GeneSymbol,行名=GeneSymbol,EntrezID保留为列供差异分析使用\n") } else { # 没有EntrezID,使用GeneSymbol expr_df <- expr_df %>% select(-ProbeID) %>% # 去掉探针ID列 select(GeneSymbol, everything()) %>% # 基因符号放第一 mutate(rowMean = rowMeans(.[, -1])) %>% # 计算平均表达量 arrange(desc(rowMean)) %>% # 按表达量降序排列 distinct(GeneSymbol, .keep_all = TRUE) %>% # 去重 select(-rowMean) %>% # 删除辅助列 column_to_rownames("GeneSymbol") # 基因符号变回行名 cat("✅ 去重使用GeneSymbol,最终结果使用GeneSymbol作为行名\n") } # 保存去重后的数据 chip_data$expr_deduped <- expr_df # 统计信息 n_before <- nrow(chip_data$expr_preprocessed) n_after <- nrow(expr_df) reduction_rate <- (n_before - n_after) / n_before * 100 chip_data$dedupe_report <- list( n_probes_before = n_before, n_genes_after = n_after, n_removed = n_before - n_after, reduction_rate = reduction_rate, n_samples = ncol(expr_df) ) cat(sprintf("✅ 探针去重完成: %d 探针 → %d 基因 (%.1f%% 减少)\n", n_before, n_after, reduction_rate)) showNotification( sprintf("✅ 去重完成!%d 探针 → %d 基因", n_before, n_after), type = "message", duration = 5 ) }, error = function(e) { showNotification(sprintf("❌ 去重失败: %s", e$message), type = "error") }) }) # 去重后统计显示 output$chip_after_dedupe_stats <- renderUI({ req(chip_data$dedupe_report) report <- chip_data$dedupe_report div( style = "font-size: 12px; color: #28a745;", tags$ul(style = "padding-left: 15px; margin: 5px 0;", tags$li(sprintf("基因数: %d", report$n_genes_after)), tags$li(sprintf("样本数: %d", report$n_samples)), tags$li(sprintf("减少探针数: %d (%.1f%%)", report$n_removed, report$reduction_rate)) ) ) }) # 去重结果显示 output$chip_dedupe_result_ui <- renderUI({ req(chip_data$dedupe_report) report <- chip_data$dedupe_report tagList( tags$table( class = "table table-striped", tags$thead( tags$tr( tags$th("项目"), tags$th("值") ) ), tags$tbody( tags$tr( tags$td("去重前探针数"), tags$td(report$n_probes_before) ), tags$tr( tags$td("去重后基因数"), tags$td(report$n_genes_after) ), tags$tr( tags$td("减少的探针数"), tags$td(report$n_removed) ), tags$tr( tags$td("减少比例"), tags$td(sprintf("%.1f%%", report$reduction_rate)) ), tags$tr( tags$td("样本数"), tags$td(report$n_samples) ) ) ), br(), h6("📊 去重后表达矩阵预览(前5行 × 前10列):", style = "color: #666;"), # 显示去重后的矩阵预览 DTOutput("chip_deduped_matrix_preview") ) }) # 去重后矩阵预览表格 output$chip_deduped_matrix_preview <- renderDT({ req(chip_data$expr_deduped) preview_df <- head(chip_data$expr_deduped, 5) if (ncol(preview_df) > 10) { preview_df <- preview_df[, 1:10] } datatable( as.data.frame(preview_df), options = list( dom = 't', paging = FALSE, scrollX = TRUE ), rownames = TRUE, filter = 'none' ) %>% formatStyle(columns = 1:ncol(preview_df), fontSize = '85%') }) # 步骤3: 生成标准格式数据 observeEvent(input$chip_generate_standard_data, { req(chip_data$expr_deduped) showNotification("🚀 正在生成标准格式数据...", type = "message") tryCatch({ # 保存为标准格式(可直接用于现有分析模块) chip_data$standard_expression <- chip_data$expr_deduped # 保存样本名称 chip_data$sample_names <- colnames(chip_data$expr_deduped) # 保存基因名称 chip_data$gene_names <- rownames(chip_data$expr_deduped) # 标记数据已准备好 chip_data$ready_for_analysis <- TRUE cat(sprintf("✅ 标准格式数据已生成: %d 基因 × %d 样本\n", nrow(chip_data$standard_expression), ncol(chip_data$standard_expression))) showNotification( sprintf("✅ 标准格式数据已生成!%d 基因 × %d 样本", nrow(chip_data$standard_expression), ncol(chip_data$standard_expression)), type = "message", duration = 10 ) }, error = function(e) { showNotification(sprintf("❌ 生成失败: %s", e$message), type = "error") }) }) # 标准数据摘要显示 output$chip_standard_data_summary <- renderUI({ req(chip_data$standard_expression) tagList( h6("📊 数据摘要", style = "color: #155724;"), tags$table( class = "table table-striped", tags$thead( tags$tr( tags$th("项目"), tags$th("值") ) ), tags$tbody( tags$tr( tags$td("基因数"), tags$td(nrow(chip_data$standard_expression)) ), tags$tr( tags$td("样本数"), tags$td(ncol(chip_data$standard_expression)) ), tags$tr( tags$td("样本名称"), tags$td(paste(head(chip_data$sample_names, 5), collapse = ", ")) ) ) ), br(), h6("✅ 现在可以进行以下分析:", style = "color: #155724;"), tags$ul(style = "padding-left: 20px;", tags$li("切换到“差异分析”模块进行limma分析"), tags$li("使用样本分组功能设置对照组和处理组"), tags$li("进行KEGG和GO富集分析"), tags$li("生成火山图和其他可视化") ) ) }) # ============================================ # 原有的分组逻辑继续 # ============================================ # 动态生成分组 UI output$chip_grouping_ui <- renderUI({ req(chip_data$series_matrix) sample_names <- colnames(chip_data$series_matrix) # 如果自动检测到分组 if (!is.null(chip_data$group_info) && !is.null(chip_data$group_info$pattern_name)) { tagList( div( class = "alert alert-info", h5("✅ 自动检测到分组模式"), p(sprintf("模式: %s", chip_data$group_info$pattern_name)), p(sprintf("对照组: %s", paste(chip_data$group_info$ctrl_samples, collapse = ", "))), p(sprintf("处理组: %s", paste(chip_data$group_info$trt_samples, collapse = ", "))), checkboxInput("chip_use_auto_groups", "使用自动检测的分组", value = TRUE) ), conditionalPanel( condition = "!input.chip_use_auto_groups", h5("手动选择分组:"), helpText("💡 提示:可以点击输入框后,直接粘贴样本名称(用逗号或空格分隔)"), fluidRow( column(6, selectizeInput("chip_ctrl_samples", "对照组样本:", choices = sample_names, multiple = TRUE, options = list(create = TRUE)) ), column(6, selectizeInput("chip_trt_samples", "处理组样本:", choices = sample_names, multiple = TRUE, options = list(create = TRUE)) ) ) ) ) } else { # 手动分组 tagList( div( class = "alert alert-warning", h5("⚠️ 未能自动检测分组模式"), p("请使用上方的快速粘贴功能,或手动指定对照组和处理组样本。") ), helpText("💡 提示:可以点击输入框后,直接粘贴样本名称(用逗号或空格分隔)"), fluidRow( column(6, selectizeInput("chip_ctrl_samples", "对照组样本:", choices = sample_names, multiple = TRUE, options = list(create = TRUE)) ), column(6, selectizeInput("chip_trt_samples", "处理组样本:", choices = sample_names, multiple = TRUE, options = list(create = TRUE)) ) ) ) } }) # 显示当前分组状态 output$chip_current_groups_ui <- renderUI({ req(chip_data$series_matrix) # 检查是否有手动设置的分组 has_manual <- !is.null(chip_data$manual_ctrl_samples) && !is.null(chip_data$manual_trt_samples) # 检查是否有自动检测的分组 has_auto <- !is.null(chip_data$group_info) && !is.null(chip_data$group_info$pattern_name) if (has_manual) { div( class = "alert alert-success", h5("✅ 当前分组(手动设置)"), p(sprintf("对照组 (%d个): %s", length(chip_data$manual_ctrl_samples), paste(chip_data$manual_ctrl_samples, collapse = ", "))), p(sprintf("处理组 (%d个): %s", length(chip_data$manual_trt_samples), paste(chip_data$manual_trt_samples, collapse = ", "))) ) } else if (has_auto) { div( class = "alert alert-info", h5("🤖 当前分组(自动检测)"), p(sprintf("模式: %s", chip_data$group_info$pattern_name)), p(sprintf("对照组 (%d个): %s", length(chip_data$group_info$ctrl_samples), paste(chip_data$group_info$ctrl_samples, collapse = ", "))), p(sprintf("处理组 (%d个): %s", length(chip_data$group_info$trt_samples), paste(chip_data$group_info$trt_samples, collapse = ", "))) ) } else { div( class = "alert alert-warning", h5("⚠️ 尚未设置分组"), p("请使用上方的快速粘贴功能设置分组,或使用手动选择。") ) } }) # 手动分组UI(折叠状态) output$chip_manual_grouping_ui <- renderUI({ req(chip_data$series_matrix) sample_names <- colnames(chip_data$series_matrix) tagList( h5("📝 手动选择样本(备选方案)", style = "color: #6E6E73;"), helpText("如果快速粘贴不方便,可以在这里手动选择样本:"), fluidRow( column(6, selectInput("chip_ctrl_samples_manual", "对照组样本:", choices = sample_names, multiple = TRUE) ), column(6, selectInput("chip_trt_samples_manual", "处理组样本:", choices = sample_names, multiple = TRUE) ) ), actionButton("chip_apply_manual_groups", "✅ 应用手动选择的分组", class = "btn-success", style = "width: 100%; margin-top: 10px;") ) }) # 应用手动选择的分组 observeEvent(input$chip_apply_manual_groups, { req(input$chip_ctrl_samples_manual) req(input$chip_trt_samples_manual) if (length(input$chip_ctrl_samples_manual) == 0 || length(input$chip_trt_samples_manual) == 0) { showNotification("请至少为每组选择一个样本!", type = "warning") return(NULL) } chip_data$manual_ctrl_samples <- input$chip_ctrl_samples_manual chip_data$manual_trt_samples <- input$chip_trt_samples_manual showNotification( sprintf("✅ 已应用手动分组: %d 对照 + %d 处理", length(input$chip_ctrl_samples_manual), length(input$chip_trt_samples_manual)), type = "message" ) }) # 运行差异分析 observeEvent(input$run_chip_analysis, { req(chip_data$series_matrix) # 获取分组信息(优先级:手动粘贴 > 自动检测 > 下拉选择) if (!is.null(chip_data$manual_ctrl_samples) && !is.null(chip_data$manual_trt_samples)) { # 使用手动粘贴的分组 ctrl_samples <- chip_data$manual_ctrl_samples trt_samples <- chip_data$manual_trt_samples cat("✅ 使用手动粘贴的分组\n") } else if (!is.null(input$chip_use_auto_groups) && input$chip_use_auto_groups && !is.null(chip_data$group_info) && !is.null(chip_data$group_info$pattern_name)) { # 使用自动检测的分组 ctrl_samples <- chip_data$group_info$ctrl_samples trt_samples <- chip_data$group_info$trt_samples cat("✅ 使用自动检测的分组\n") } else { # 使用下拉选择的分组 ctrl_samples <- input$chip_ctrl_samples trt_samples <- input$chip_trt_samples cat("✅ 使用下拉选择的分组\n") } # 验证分组 if (is.null(ctrl_samples) || length(ctrl_samples) == 0 || is.null(trt_samples) || length(trt_samples) == 0) { showNotification("请先设置对照组和处理组样本!", type = "error") return(NULL) } # 显示进度 showNotification("正在运行差异分析...", type = "message") # ✅ 优先使用经过完整预处理流程的数据 if (!is.null(chip_data$standard_expression) && chip_data$ready_for_analysis) { # 使用标准格式数据(已探针注释、预处理、去重) expr_matrix <- chip_data$standard_expression cat("✅ 使用标准格式数据(已探针注释和去重)\n") cat(sprintf(" 表达矩阵: %d 基因 × %d 样本\n", nrow(expr_matrix), ncol(expr_matrix))) } else if (!is.null(chip_data$expr_deduped)) { # 使用去重后的数据 expr_matrix <- chip_data$expr_deduped cat("✅ 使用去重后的表达数据\n") } else if (!is.null(chip_data$merged_matrix)) { # 使用合并后的数据(探针已注释,但未去重) # 需要去掉ProbeID和Gene列 merged_df <- chip_data$merged_matrix # 识别数值列(样本数据) numeric_cols <- sapply(merged_df, function(x) is.numeric(x)) expr_matrix <- as.matrix(merged_df[, numeric_cols, drop = FALSE]) # 使用ProbeID作为行名(因为可能还需要探针级别的信息) if ("ProbeID" %in% colnames(merged_df)) { rownames(expr_matrix) <- merged_df$ProbeID } else { rownames(expr_matrix) <- merged_df$Gene } cat("✅ 使用合并后的数据(探针已注释)\n") } else { # 最后的备选方案:使用原始Series Matrix probe_mapping <- chip_data$probe_mapping if (is.null(probe_mapping)) { # 如果没有加载注释文件,使用探针ID作为基因符号 expr_matrix <- chip_data$series_matrix cat("⚠️ 未加载注释文件,使用探针ID作为基因符号\n") } else { # 使用注释映射(旧的自动检测方法) expr_matrix <- aggregate_probe_expression( chip_data$series_matrix, probe_mapping ) if (is.null(expr_matrix)) { showNotification("探针注释失败!", type = "error") return(NULL) } } } # 运行 limma 分析 limma_res <- run_limma_analysis( expr_matrix = expr_matrix, ctrl_samples = ctrl_samples, trt_samples = trt_samples, logfc_threshold = input$chip_logfc_threshold, pvalue_threshold = input$chip_pvalue_threshold, pval_type = input$chip_pval_type # 🔧 传入用户选择的P值类型 ) if (is.null(limma_res)) { showNotification("差异分析失败!", type = "error") return(NULL) } # 转换为标准格式 formatted_results <- format_chip_results_for_pipeline( limma_res, expr_matrix, ctrl_samples, trt_samples ) # 保存结果到 reactiveValues chip_data$limma_results <- limma_res chip_data$formatted_results <- formatted_results showNotification( sprintf("✅ 分析完成: %d 个显著差异基因", limma_res$n_significant), type = "message" ) }) # 显示结果 output$chip_results_ui <- renderUI({ req(chip_data$limma_results) limma_res <- chip_data$limma_results tagList( # 统计摘要 fluidRow( column(3, wellPanel( style = "background: #f8f9fa; border: 2px solid #dee2e6; text-align: center; padding: 20px;", h3(limma_res$n_total, style = "color: #495057; margin: 10px 0;"), h6("总基因数", style = "color: #6c757d; margin: 0;") ) ), column(3, wellPanel( style = "background: #e7f3ff; border: 2px solid #007bff; text-align: center; padding: 20px;", h3(limma_res$n_significant, style = "color: #007bff; margin: 10px 0;"), icon("star", style = "color: #007bff; font-size: 24px;"), h6("显著差异", style = "color: #007bff; margin: 5px 0 0 0;") ) ), column(3, wellPanel( style = "background: #d4edda; border: 2px solid #28a745; text-align: center; padding: 20px;", h3(limma_res$n_up, style = "color: #28a745; margin: 10px 0;"), icon("arrow-up", style = "color: #28a745; font-size: 24px;"), h6("上调", style = "color: #28a745; margin: 5px 0 0 0;") ) ), column(3, wellPanel( style = "background: #f8d7da; border: 2px solid #dc3545; text-align: center; padding: 20px;", h3(limma_res$n_down, style = "color: #dc3545; margin: 10px 0;"), icon("arrow-down", style = "color: #dc3545; font-size: 24px;"), h6("下调", style = "color: #dc3545; margin: 5px 0 0 0;") ) ) ), hr(), # 结果表格 h4("差异分析结果"), DTOutput("chip_results_table"), br(), # 下载按钮 downloadButton("download_chip_results", "📥 下载结果", class = "btn-success") ) }) # 结果表格 output$chip_results_table <- renderDT({ req(chip_data$limma_results) results <- chip_data$limma_results$results # 🔧 添加显著性标记 - 根据用户选择的P值类型 pval_col <- if (input$chip_pval_type == "adj.P.Val") "adj.P.Val" else "P.Value" results$Significant <- ifelse( results[[pval_col]] < input$chip_pvalue_threshold & abs(results$logFC) >= input$chip_logfc_threshold, "Yes", "No" ) # 🔧 修复:重新排列列顺序,确保ID和SYMBOL列在前 # 期望顺序:ID, SYMBOL, logFC, AveExpr, t, P.Value, adj.P.Val, B, Significant results_ordered <- results[, c("ID", "SYMBOL", "logFC", "AveExpr", "t", "P.Value", "adj.P.Val", "B", "Significant")] # 🔍 诊断:检查ID列的内容 id_sample <- head(results_ordered$ID, 5) cat(sprintf("📊 差异分析结果ID列示例: %s\n", paste(id_sample, collapse = ", "))) cat(sprintf("📊 ID列类型: %s\n", class(results_ordered$ID)[1])) # 检查ID列是否为基因符号(包含字母)还是EntrezID(纯数字) is_entrez_id <- all(grepl("^[0-9]+$", results_ordered$ID[!is.na(results_ordered$ID)])) if (!is_entrez_id) { cat("⚠️ 警告: ID列包含基因符号而非Entrez Gene ID!\n") cat("💡 这可能是因为SOFT文件中缺少EntrezID列\n") } datatable( results_ordered, options = list( pageLength = 25, scrollX = TRUE, order = list(list(6, 'asc')) # 按P.Value排序(第6列) ), filter = 'top', rownames = FALSE ) %>% formatRound(columns = c('logFC', 'AveExpr', 't', 'P.Value', 'adj.P.Val'), digits = 4) %>% formatStyle( columns = c('ID', 'SYMBOL'), backgroundColor = '#e8f4f8', fontWeight = 'bold' ) %>% formatStyle( 'Significant', color = styleEqual(c('Yes', 'No'), c('green', 'grey')), fontWeight = 'bold' ) }) # 下载结果 output$download_chip_results <- downloadHandler( filename = function() { sprintf("chip_analysis_results_%s.csv", Sys.Date()) }, content = function(file) { req(chip_data$limma_results) results <- chip_data$limma_results$results write.csv(results, file, row.names = FALSE) } ) } # ===================================================== # 模块结束 # ===================================================== # ===================================================== # 7. 辅助函数:解析粘贴的样本列表 # ===================================================== #' 解析粘贴的样本列表(每行一个样本名) #' #' @param pasted_text 粘贴的文本内容 #' @param expr_matrix 表达矩阵(用于验证样本名) #' @return vector 样本名向量 parse_sample_list <- function(pasted_text, expr_matrix) { cat("🔍 开始解析样本列表...\n") # 分割成行 lines <- strsplit(pasted_text, "\n")[[1]] lines <- trimws(lines) # 移除首尾空白 lines <- lines[lines != ""] # 移除空行 if (length(lines) == 0) { cat("⚠️ 粘贴内容为空\n") return(NULL) } cat(sprintf("📊 读取到 %d 行\n", length(lines))) # 获取表达矩阵的样本名 matrix_samples <- colnames(expr_matrix) # 匹配样本名 valid_samples <- intersect(lines, matrix_samples) if (length(valid_samples) == 0) { cat("⚠️ 未找到匹配的样本\n") cat(sprintf(" 粘贴的样本: %s\n", paste(head(lines, 3), collapse = ", "))) cat(sprintf(" 可用样本: %s\n", paste(head(matrix_samples, 3), collapse = ", "))) return(NULL) } cat(sprintf("✅ 匹配成功: %d / %d 个样本\n", length(valid_samples), length(lines))) # 检查是否有未匹配的样本 unmatched <- setdiff(lines, matrix_samples) if (length(unmatched) > 0) { cat(sprintf("⚠️ %d 个样本未匹配: %s\n", length(unmatched), paste(head(unmatched, 3), collapse = ", "))) } return(valid_samples) } #' 解析用户粘贴的分组信息(旧版本,保留备用) #' #' @param pasted_text 粘贴的文本内容 #' @param group_col_name 分组列名(可选) #' @param expr_matrix 表达矩阵(用于验证样本名) #' @return list 包含 ctrl_samples 和 trt_samples parse_pasted_groups <- function(pasted_text, group_col_name, expr_matrix) { cat("🔍 开始解析粘贴的分组信息...\n") # 分割成行 lines <- strsplit(pasted_text, "\n")[[1]] lines <- lines[lines != ""] # 移除空行 if (length(lines) == 0) { return(list(success = FALSE, error = "粘贴内容为空")) } # 方法1: 如果指定了分组列名,按列名解析 if (!is.null(group_col_name) && group_col_name != "") { cat(sprintf("📋 使用分组列名: %s\n", group_col_name)) # 尝试读取为表格 tryCatch({ # 读取文本 text_conn <- textConnection(lines) df <- read.table(text_conn, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, quote = "\"", comment.char = "") close(text_conn) # 检查是否存在分组列 if (!group_col_name %in% colnames(df)) { return(list(success = FALSE, error = sprintf("未找到分组列 '%s',可用列: %s", group_col_name, paste(colnames(df), collapse = ", ")))) } # 提取分组信息 groups <- df[[group_col_name]] # 获取唯一分组 unique_groups <- unique(groups) if (length(unique_groups) < 2) { return(list(success = FALSE, error = "分组列中只有一个组,需要至少2个组")) } # 如果超过2个组,取前2个 if (length(unique_groups) > 2) { cat(sprintf("⚠️ 检测到 %d 个组,将使用前两个: %s\n", length(unique_groups), paste(unique_groups[1:2], collapse = ", "))) unique_groups <- unique_groups[1:2] } # 根据分组列提取样本 ctrl_name <- unique_groups[1] trt_name <- unique_groups[2] ctrl_samples <- colnames(df)[groups == ctrl_name] trt_samples <- colnames(df)[groups == trt_name] # 移除分组列本身 ctrl_samples <- ctrl_samples[ctrl_samples != group_col_name] trt_samples <- trt_samples[trt_samples != group_col_name] cat(sprintf("✅ 解析成功: %d 对照 (%s) + %d 处理 (%s)\n", length(ctrl_samples), ctrl_name, length(trt_samples), trt_name)) return(list( success = TRUE, ctrl_samples = ctrl_samples, trt_samples = trt_samples )) }, error = function(e) { return(list(success = FALSE, error = paste("解析表格失败:", e$message))) }) } # 方法2: 没有指定列名,自动检测 cat("🤖 自动检测分组模式...\n") # 尝试检测第一行是否为列名 first_line <- lines[1] tryCatch({ # 读取为表格 text_conn <- textConnection(lines) df <- read.table(text_conn, header = TRUE, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE, quote = "\"", comment.char = "") close(text_conn) # 获取所有样本名(列名,排除第一列ID列) all_samples <- colnames(df)[-1] # 排除ID列 # 获取表达矩阵的样本名 matrix_samples <- colnames(expr_matrix) # 找到交集 common_samples <- intersect(all_samples, matrix_samples) if (length(common_samples) < 2) { return(list(success = FALSE, error = sprintf("在粘贴内容中只找到 %d 个有效样本,需要至少2个", length(common_samples)))) } cat(sprintf("📊 找到 %d 个有效样本\n", length(common_samples))) # 尝试从列名中自动分组 # 使用现有的自动检测函数 sample_names <- common_samples # 如果有多余的列,尝试从第二列开始检测分组模式 if (ncol(df) > 2) { # 检查第二列是否有分组信息 potential_groups <- df[[2]] if (length(unique(potential_groups)) == 2) { unique_g <- unique(potential_groups) ctrl_samples <- sample_names[potential_groups == unique_g[1]] trt_samples <- sample_names[potential_groups == unique_g[2]] cat(sprintf("✅ 自动检测分组: %d 对照 + %d 处理\n", length(ctrl_samples), length(trt_samples))) return(list( success = TRUE, ctrl_samples = ctrl_samples, trt_samples = trt_samples )) } } # 如果无法自动检测,返回所有样本让用户手动选择 cat("⚠️ 无法自动检测分组,返回所有样本供手动选择\n") # 默认:前一半作为对照,后一半作为处理 n_samples <- length(sample_names) mid_point <- ceiling(n_samples / 2) ctrl_samples <- sample_names[1:mid_point] trt_samples <- sample_names[(mid_point+1):n_samples] cat(sprintf("⚠️ 默认分组: 前 %d 个为对照,后 %d 个为处理\n", length(ctrl_samples), length(trt_samples))) return(list( success = TRUE, ctrl_samples = ctrl_samples, trt_samples = trt_samples, warning = "无法自动检测分组模式,已按位置默认分组,请手动调整" )) }, error = function(e) { return(list(success = FALSE, error = paste("解析失败:", e$message))) }) }