Files
2026-08-03 18:42:47 +08:00

271 lines
8.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
表达分析Pipeline
整合差异表达分析 + 功能注释
自动适配小/大家族
"""
import os
import subprocess
import pandas as pd
import numpy as np
from collections import defaultdict
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from pathlib import Path
@dataclass
class ExpressionConfig:
"""表达分析配置"""
fpkm_file: str = ""
sample_file: str = ""
gene_list: str = ""
output_prefix: str = "expression"
r_script: str = "scripts/draw_expression.R"
# 差异表达参数
min_log2fc: float = 1.0 # 最小log2 fold change
min_expression: float = 1.0 # 最小表达量
# 功能注释
gene2go_file: str = ""
@dataclass
class ExpressionResult:
"""表达分析结果"""
exp_file: str # 表达量矩阵
diff_genes_x3: List[str] = field(default_factory=list) # X3差异基因
diff_genes_x4: List[str] = field(default_factory=list) # X4差异基因
all_diff_genes: List[str] = field(default_factory=list) # 所有差异基因
gene2go_file: str = ""
plots: List[str] = field(default_factory=list)
# 统计
total_genes: int = 0
diff_count_x3: int = 0
diff_count_x4: int = 0
def run_r_expression(config: ExpressionConfig) -> ExpressionResult:
"""运行R脚本进行差异表达分析"""
# 准备基因列表(去.T01后缀)
if config.gene_list:
gene_list_new = config.gene_list + "_new"
with open(config.gene_list) as f:
genes = [g.strip() for g in f if g.strip()]
genes_new = [g.replace('.T01', '') for g in genes]
with open(gene_list_new, 'w') as f:
f.write('\n'.join(genes_new))
config.gene_list = gene_list_new
# 运行R脚本
cmd = [
"Rscript", config.r_script,
config.fpkm_file,
config.gene_list,
config.sample_file,
config.output_prefix
]
# 确保使用正确的R
r_exe = "/home/xzq/miniconda3/envs/rnaR/bin/Rscript"
if os.path.exists(r_exe):
cmd[0] = r_exe
result = subprocess.run(cmd, capture_output=True, text=True, cwd=os.path.dirname(config.fpkm_file) or '.')
if result.returncode != 0:
print(f"R脚本警告: {result.stderr}")
# 读取结果文件
exp_file = config.output_prefix + ".gene-exp.txt"
if not os.path.exists(exp_file):
exp_file = os.path.join(os.path.dirname(config.fpkm_file), config.output_prefix + ".gene-exp.txt")
return exp_file
def find_differential_genes(exp_file: str, config: ExpressionConfig) -> ExpressionResult:
"""筛选差异表达基因"""
# 读取表达量
if not os.path.exists(exp_file):
raise FileNotFoundError(f"表达量文件不存在: {exp_file}")
exp = pd.read_csv(exp_file, sep='\t', index_col=0)
# 计算各组平均表达量
sample_info = {}
with open(config.sample_file) as f:
for line in f:
parts = line.strip().split('\t')
if len(parts) >= 2:
sample_info[parts[0]] = parts[1]
# 识别对照组和处理组
groups = set(sample_info.values())
if len(groups) < 2:
raise ValueError("样本分组不足")
control = min(groups) # 假设字母序最小的是对照
treatments = [g for g in groups if g != control]
# 计算平均表达量
for group in groups:
cols = [c for c in exp.columns if sample_info.get(c, '').startswith(group)]
if cols:
exp[f'{group}_mean'] = exp[cols].mean(axis=1)
# 计算log2FC
for treat in treatments:
if f'{treat}_mean' in exp.columns:
exp[f'log2FC_{treat}vs{control}'] = np.log2((exp[f'{treat}_mean'] + 0.1) / (exp[f'{control}_mean'] + 0.1))
# 筛选差异基因
diff_x3 = set()
diff_x4 = set()
if 'log2FC_X3vsX2' in exp.columns:
diff_x3 = set(exp[abs(exp['log2FC_X3vsX2']) >= config.min_log2fc].index)
if 'log2FC_X4vsX2' in exp.columns:
diff_x4 = set(exp[abs(exp['log2FC_X4vsX2']) >= config.min_log2fc].index)
all_diff = diff_x3 | diff_x4
# 过滤低表达基因
exp = exp[exp[[c for c in exp.columns if '_mean' in c]].max(axis=1) >= config.min_expression]
result = ExpressionResult(
exp_file=exp_file,
diff_genes_x3=sorted(diff_x3),
diff_genes_x4=sorted(diff_x4),
all_diff_genes=sorted(all_diff),
total_genes=len(exp),
diff_count_x3=len(diff_x3),
diff_count_x4=len(diff_x4)
)
# 查找生成的图片
out_dir = os.path.dirname(exp_file) or '.'
result.plots = [
f for f in os.listdir(out_dir)
if f.startswith(config.output_prefix) and f.endswith('.pdf')
]
return result
def add_functional_annotation(result: ExpressionResult, gene2go_file: str) -> ExpressionResult:
"""添加功能注释"""
result.gene2go_file = gene2go_file
if not os.path.exists(gene2go_file):
return result
# 读取GO注释
gene2go = defaultdict(list)
with open(gene2go_file) as f:
for line in f:
gene, go = line.strip().split('\t')
gene2go[gene].append(go)
result.gene2go = dict(gene2go)
return result
def generate_summary_report(result: ExpressionResult, output_file: str):
"""生成分析报告"""
with open(output_file, 'w') as f:
f.write("=" * 60 + "\n")
f.write("基因家族表达分析报告\n")
f.write("=" * 60 + "\n\n")
f.write(f"总基因数: {result.total_genes}\n")
f.write(f"差异基因 (X3 vs X2): {result.diff_count_x3}\n")
f.write(f"差异基因 (X4 vs X2): {result.diff_count_x4}\n\n")
if result.diff_genes_x3:
f.write("差异基因列表 (X3 vs X2):\n")
for g in result.diff_genes_x3:
f.write(f" {g}\n")
f.write("\n")
if result.diff_genes_x4:
f.write("差异基因列表 (X4 vs X2):\n")
for g in result.diff_genes_x4:
f.write(f" {g}\n")
f.write("\n")
# 功能注释统计
if hasattr(result, 'gene2go') and result.gene2go:
f.write("功能注释统计:\n")
diff_with_go = sum(1 for g in result.all_diff_genes if g in result.gene2go)
f.write(f" 差异基因有GO注释: {diff_with_go}/{len(result.all_diff_genes)}\n")
if result.plots:
f.write("\n生成的图表:\n")
for p in result.plots:
f.write(f" - {p}\n")
print(f"报告已保存: {output_file}")
def run_expression_pipeline(
fpkm_file: str,
sample_file: str,
gene_list: str,
gene2go_file: str = "",
output_dir: str = ".",
output_prefix: str = "expression"
) -> ExpressionResult:
"""
运行完整的表达分析Pipeline
"""
config = ExpressionConfig(
fpkm_file=fpkm_file,
sample_file=sample_file,
gene_list=gene_list,
output_prefix=output_prefix
)
# 1. 运行R差异表达分析
print("[表达] 运行差异表达分析...")
exp_file = run_r_expression(config)
# 2. 筛选差异基因
print("[表达] 筛选差异基因...")
result = find_differential_genes(exp_file, config)
# 3. 添加功能注释
if gene2go_file and os.path.exists(gene2go_file):
print("[表达] 添加功能注释...")
result = add_functional_annotation(result, gene2go_file)
# 4. 生成报告
report_file = os.path.join(output_dir, f"{output_prefix}_report.txt")
generate_summary_report(result, report_file)
print(f"\n=== 分析完成 ===")
print(f"总基因: {result.total_genes}")
print(f"差异基因 X3vsX2: {result.diff_count_x3}")
print(f"差异基因 X4vsX2: {result.diff_count_x4}")
return result
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='表达分析Pipeline')
parser.add_argument('--fpkm', required=True, help='FPKM表达量文件')
parser.add_argument('--sample', required=True, help='样本分组文件')
parser.add_argument('--genes', required=True, help='基因ID列表')
parser.add_argument('--gene2go', default='', help='GO注释文件')
parser.add_argument('--output-dir', default='.', help='输出目录')
parser.add_argument('--prefix', default='expression', help='输出前缀')
args = parser.parse_args()
run_expression_pipeline(
args.fpkm, args.sample, args.genes, args.gene2go,
args.output_dir, args.prefix
)