当前位置:首页>python>还在为转录组分析熬夜调图?这个Python工具能让你提前下班,顺便搞定SCI论文“研究方法”部分!

还在为转录组分析熬夜调图?这个Python工具能让你提前下班,顺便搞定SCI论文“研究方法”部分!

  • 2026-08-18 23:11:24
还在为转录组分析熬夜调图?这个Python工具能让你提前下班,顺便搞定SCI论文“研究方法”部分!

当你还在手动处理表达矩阵、苦苦调试火山图的字体大小时,隔壁实验室的小王已经用这套流程三天内完成了从原始数据到论文初稿的跨越。他到底用了什么“黑科技”?今天,我就把这套一键式转录组QC + 差异表达分析管线拆解给你看,文末还附赠一个写SCI“研究方法”部分的小心机,让你既省时又显得专业!

研究背景:转录组分析,最“磨人”的不是生物学问题,而是出图

转录组测序(RNA-seq)早已成为生命科学研究的标配。但拿到测序公司返回的表达矩阵后,真正的“噩梦”才开始:

  • 数据质控:样本表达分布是否正常?是否存在批次效应?

  • 校正前后对比:用了ComBat、RUV还是其他校正方法,效果到底怎么样?

  • 差异表达分析:如何合理纳入批次协变量,避免假阳性?

  • 可视化:火山图、MA图、热图、箱线图……每个图都要调字体、调颜色、调图例,更要命的是,期刊要求的格式还各不相同(有的要PDF矢量,有的要TIFF 600 dpi)。

这些技术工作,占据了科研人员大量时间,却几乎不贡献科学见解。能不能自动化、标准化,甚至智能化?

我们的研究内容:一个纯Python管线,让你的分析“一步到位”

我们开发了一个名为 sci_deg_pipeline 的整合式分析脚本,专门解决上述痛点。它做的事情很简单,但很彻底:

1. 智能预处理,省去手动判断

  • 自动识别表达量级,智能决定是否进行log2转换(无需用户纠结)。

  • 自动过滤缺失率过高的基因,并对重复基因ID取均值。

  • 样本名解析:只要遵循 Project_Sample_Group 格式(如 GSE37157_GSM123456_Control),自动提取项目、样本和分组信息,无需额外提供元数据文件

2. 完整的QC对比图(校正前后)

  • 如果用户提供了“校正前”和“校正后”两个矩阵,脚本会自动生成并排的箱线图和PCA图,直观展示校正效果(Figure 1 & 2)。

  • PCA图中自动添加95%置信椭圆,且按项目着色、按分组使用不同标记,一目了然

3. 协变量校正的差异表达模型

  • 采用普通最小二乘(OLS)回归,模型为:表达 ~ 分组 + 项目(Project)

  • 这样可以在估计组间差异(logFC)的同时,剥离项目间的批次差异,提高统计准确性。

  • 输出完整的统计结果:logFC、平均表达、标准误、t统计量、原始P值、FDR校正P值。

4. 差异基因一站式可视化

  • 火山图(标注top基因,自动避让文本)、MA图DEG统计条图P值分布直方图,四合一放在一张大图中(Figure 3)。

  • 热图展示前50上调和前50下调基因的表达模式(Figure 4)。

  • 箱线图展示top基因在两组间的表达分布,并叠加抖动散点(Figure 5)。

5. 出版级图片与完整结果输出

  • 所有图片字体强制为 Times New Roman,符合多数期刊要求。

  • 同时输出矢量(PDF/SVG)和高分辨率栅格(PNG/TIFF)格式,投稿无压力

  • 除图片外,还会输出差异表达全表、显著基因表、设计矩阵、表达矩阵等,数据完整可追溯

研究方法(以脚本中的核心分析为例):如何写出专业且清晰的SCI“方法”描述

很多同学在做完分析后,不知道如何在论文的“Materials and Methods”中准确描述自己的数据处理步骤。这里我以本脚本中的差异表达分析方法为例,展示一段符合SCI规范的写法,你可以直接参考或修改使用。

参考写法(英文,但你可以译成中文):

Differential expression analysisTo identify genes differentially expressed between treatment and control groups while accounting for potential batch effects from different projects, we fit a per‑gene ordinary least squares (OLS) linear model of the form:Expression = β₀ + β₁·Group + Σ γⱼ·Projectⱼ + εwhere Group is a binary indicator (treatment vs. control), and Projectⱼ are dummy variables for each project (with the first project as reference). The coefficient β₁ represents the log₂ fold change.  

Raw p‑values were obtained from a t‑statistic with residual degrees of freedom, and multiple testing was controlled using the Benjamini‑Hochberg false discovery rate (FDR) procedure. Genes with |log₂FC| > 0.585 and FDR < 0.05 were considered differentially expressed.  

Prior to analysis, expression values were automatically log₂‑transformed if the data scale indicated a need (i.e., 99th percentile > 100 or interquartile range > 50 with median > 10). Genes with more than 20% missing values were filtered out, and remaining missing values were imputed by gene‑wise medians.

为什么这样写能加分?

  • 明确模型公式:让审稿人清楚你如何控制批次效应,不是简单使用默认参数。

  • 量化阈值:给出具体的logFC和FDR阈值,体现严谨性。

  • 说明预处理细节(自动log2、缺失值处理),避免被质疑数据处理不当。

  • 点明统计方法(OLS、t检验、BH校正),彰显专业度。

这个小段落,你可以直接嵌入你的手稿中,并根据实际使用的阈值微调。

如何使用这个工具?(一键运行)

你只需准备一个基因×样本的TSV表达矩阵,然后在终端执行:

python sci_deg_pipeline.py \
--norm normalized_expression.tsv \
--pre raw_expression.tsv \       # 如果有校正前矩阵
--outdir results \
--control Control \
--treat Treatment \
--formats pdf png tiff

脚本会自动跑完全部流程,所有图片和表格都会整齐地存放在 results/ 目录下。

核心优势总结

维度优势
自动化从数据读取到最终图片,全自动,无需手动干预
统计严谨模型纳入批次协变量,避免假阳性
可视化专业统一Times New Roman,矢量/栅格双输出,直接用于投稿
结果完整既得图表,又得数据表,便于后续挖掘
可复现命令行参数可记录,保证分析的可复现性

最后:一个让你“研究方法”部分更出彩的小贴士

除了描述统计模型,别忘了在方法中提及软件环境和关键依赖库版本,例如:

“All analyses were performed using Python 3.9 with the following packages: pandas 1.5, numpy 1.23, matplotlib 3.6, seaborn 0.12, and statsmodels 0.13. The custom pipeline is available at [GitHub link].”

这样既增加了透明性,也方便他人复现,审稿人看了都会暗暗点头。

立即体验,让你的分析效率翻倍!

不再纠结于调图和写重复代码,把时间留给真正重要的生物学问题。如果你对这个工具感兴趣,欢迎在评论区留言,我会分享完整代码和使用示例。

快转发给你的实验室伙伴,一起告别“手动挡”分析时代!

关注我,获取更多生信实用工具和论文写作干货!下期预告:如何用Python自动生成GSEA富集分析图表?敬请期待!

#!/usr/bin/env python3# -*- coding: utf-8 -*-"""Integrated transcriptomic QC and differential-expression pipeline.Main outputs------------1. Before/after sample-expression boxplots2. Before/after PCA plots (project color, phenotype marker, 95% ellipse)3. Differential-expression table using OLS with project covariates4. Volcano plot, MA plot, DEG summary, P-value distribution5. Top-DEG heatmap and top-gene expression boxplots6. Publication-ready PDF/PNG/TIFF/SVG figures with Times New RomanExpected sample name format---------------------------Project_Sample_GroupExamples:GSE37157_GSM123456_ControlGSE37157_GSM123457_TreatThe parser uses the first underscore-delimited field as Project, the last fieldas Group, and everything in between as Sample."""from __future__ import annotationsimport argparseimport jsonimport mathimport reimport sysimport warningsfrom dataclasses import dataclassfrom pathlib import Pathfrom typing import Dict, Iterable, ListOptionalSequenceTupleimport matplotlib as mplimport matplotlib.pyplot as pltfrom matplotlib.lines import Line2Dfrom matplotlib.patches import Ellipse, Patchfrom matplotlib import font_managerimport numpy as npimport pandas as pdimport seaborn as snsfrom scipy.stats import chi2, t as student_tfrom sklearn.decomposition import PCAfrom sklearn.preprocessing import StandardScalerfrom statsmodels.stats.multitest import multipleteststry:    from adjustText import adjust_textexcept ImportError:  # optional dependency    adjust_text = None# =============================================================================# Configuration# =============================================================================NPG_COLORS = [    "#3C5488",  # blue    "#E64B35",  # red    "#00A087",  # green    "#4DBBD5",  # cyan    "#F39B7F",  # salmon    "#8491B4",  # slate    "#91D1C2",  # mint    "#DC0000",  # deep red    "#7E6148",  # brown    "#B09C85",  # taupe]REGULATION_COLORS = {    "Up-regulated""#E64B35",    "Down-regulated""#3C5488",    "Not significant""#B8B8B8",}GROUP_MARKERS = ["o""s""^""D""P""X""v""<"">""h"]@dataclassclass PipelineConfig:    pre_file: Optional[Path]    norm_file: Path    outdir: Path    control: str = "Control"    treat: str = "Treat"    logfc_cutoff: float = 0.585    fdr_cutoff: float = 0.05    top_heatmap_each: int = 50    top_labels_each: int = 10    top_box_each: int = 3    pca_top_variable_genes: int = 5000    show_every_n: Optional[int] = None    auto_log2: bool = True    max_missing_fraction: float = 0.20    formats: Tuple[str, ...] = ("pdf""png")    raster_dpi: int = 600    seed: int = 2026# =============================================================================# Style and utility functions# =============================================================================def configure_publication_style() -> None:    """Configure a clean, journal-style visual theme."""    try:        font_manager.findfont("Times New Roman", fallback_to_default=False)    except Exception:        warnings.warn(            "Times New Roman was not found on this system. Matplotlib will use "            "a serif fallback. Install/enable Times New Roman for exact output.",            RuntimeWarning,        )    sns.set_theme(style="white", context="paper")    mpl.rcParams.update(        {            "font.family""Times New Roman",            "font.size"10,            "font.weight""normal",            "axes.labelweight""bold",            "axes.titleweight""bold",            "axes.linewidth"1.0,            "axes.spines.top"False,            "axes.spines.right"False,            "xtick.major.width"1.0,            "ytick.major.width"1.0,            "xtick.major.size"4.0,            "ytick.major.size"4.0,            "legend.frameon"False,            "pdf.fonttype"42,            "ps.fonttype"42,            "svg.fonttype""none",            "mathtext.fontset""stix",            "savefig.facecolor""white",            "figure.facecolor""white",        }    )def ensure_dirs(outdir: Path) -> Dict[str, Path]:    paths = {        "root": outdir,        "figures": outdir / "figures",        "results": outdir / "results",        "logs": outdir / "logs",    }    for path in paths.values():        path.mkdir(parents=True, exist_ok=True)    return pathsdef save_figure(    fig: mpl.figure.Figure,    stem: Path,    formats: Sequence[str],    raster_dpi: int = 600,    close: bool = True,) -> None:    """Save vector and/or raster versions using consistent settings."""    stem.parent.mkdir(parents=True, exist_ok=True)    for fmt in formats:        fmt = fmt.lower().lstrip(".")        output = stem.with_suffix(f".{fmt}")        kwargs = {"bbox_inches""tight""facecolor""white"}        if fmt in {"png""tif""tiff""jpg""jpeg"}:            kwargs["dpi"] = raster_dpi        fig.savefig(output, **kwargs)    if close:        plt.close(fig)def make_palette(categories: Iterable[str]) -> Dict[strstr]:    cats = list(dict.fromkeys(map(str, categories)))    if len(cats) <= len(NPG_COLORS):        colors = NPG_COLORS[: len(cats)]    else:        colors = [mpl.colors.to_hex(c) for c in sns.color_palette("husl"len(cats))]    return dict(zip(cats, colors))def make_marker_map(categories: Iterable[str]) -> Dict[strstr]:    cats = list(dict.fromkeys(map(str, categories)))    return {cat: GROUP_MARKERS[i % len(GROUP_MARKERS)] for i, cat in enumerate(cats)}def sparse_xticks(labels: Sequence[str], step: Optional[int] = None) -> Tuple[List[int], List[str]]:    n = len(labels)    if step is None:        if n <= 20:            step = 1        elif n <= 40:            step = 2        elif n <= 80:            step = 5        elif n <= 120:            step = 8        else:            step = 10    positions = list(range(1, n + 1max(1, step)))    tick_labels = [labels[i - 1for i in positions]    return positions, tick_labelsdef resolve_label(values: Sequence[str], requested: str) -> str:    """Resolve a group label case-insensitively while preserving original text."""    unique = list(dict.fromkeys(map(str, values)))    if requested in unique:        return requested    matches = [x for x in unique if x.lower() == requested.lower()]    if len(matches) == 1:        return matches[0]    raise ValueError(        f"Group label '{requested}' not found. Available groups: {', '.join(unique)}"    )# =============================================================================# Data loading and metadata# =============================================================================def parse_sample_metadata(sample_names: Sequence[str]) -> pd.DataFrame:    rows = []    pattern = re.compile(r"^(?P<Project>[^_]+)_(?P<Sample>.+)_(?P<Group>[^_]+)$")    for original in map(str, sample_names):        match = pattern.match(original)        if match is None:            raise ValueError(                "Unable to parse sample name: "                f"'{original}'. Expected format: Project_Sample_Group"            )        rows.append(            {                "Original": original,                "Project"match.group("Project"),                "Sample"match.group("Sample"),                "Group"match.group("Group"),            }        )    meta = pd.DataFrame(rows).set_index("Original", drop=False)    if meta.index.duplicated().any():        duplicates = meta.index[meta.index.duplicated()].tolist()        raise ValueError(f"Duplicated sample names detected: {duplicates[:5]}")    return metadef _auto_log2_transform(expr: pd.DataFrame) -> Tuple[pd.DataFrame, bool]:    values = expr.to_numpy(dtype=float)    finite = values[np.isfinite(values)]    if finite.size == 0:        return expr, False    q01, q50, q99 = np.nanpercentile(finite, [15099])    needs_log = (q99 > 100or ((q99 - q01) > 50 and q50 > 10)    if not needs_log:        return expr, False    min_value = np.nanmin(finite)    shift = 1.0 - min_value if min_value <= 0 else 0.0    transformed = np.log2(expr + shift)    return transformed, Truedef read_expression(    path: Path,    auto_log2: bool = True,    max_missing_fraction: float = 0.20,) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[strobject]]:    """Read genes x samples expression matrix and perform conservative cleaning."""    if not path.exists():        raise FileNotFoundError(f"Input file not found: {path}")    expr = pd.read_csv(path, sep="\t", header=0, index_col=0)    expr.index = expr.index.astype(str)    expr.columns = expr.columns.astype(str)    expr = expr.apply(pd.to_numeric, errors="coerce")    expr = expr.replace([np.inf, -np.inf], np.nan)    initial_shape = expr.shape    missing_fraction = expr.isna().mean(axis=1)    expr = expr.loc[missing_fraction <= max_missing_fraction].copy()    if expr.isna().any().any():        row_medians = expr.median(axis=1)        expr = expr.T.fillna(row_medians).T    # Average duplicated gene/probe identifiers.    expr = expr.groupby(level=0, sort=False).mean()    # Remove genes with no variation; they do not contribute to PCA or testing.    variances = expr.var(axis=1, ddof=1)    expr = expr.loc[variances.fillna(0) > 0].copy()    log2_applied = False    if auto_log2:        expr, log2_applied = _auto_log2_transform(expr)    metadata = parse_sample_metadata(expr.columns)    info = {        "file"str(path),        "initial_genes"int(initial_shape[0]),        "initial_samples"int(initial_shape[1]),        "final_genes"int(expr.shape[0]),        "final_samples"int(expr.shape[1]),        "log2_applied"bool(log2_applied),        "max_missing_fraction"float(max_missing_fraction),    }    return expr, metadata, info# =============================================================================# PCA and confidence ellipses# =============================================================================def add_confidence_ellipse(    ax: mpl.axes.Axes,    x: np.ndarray,    y: np.ndarray,    color: str,    level: float = 0.95,) -> None:    if len(x) < 3 or len(y) < 3:        return    cov = np.cov(x, y)    if not np.all(np.isfinite(cov)):        return    eigenvalues, eigenvectors = np.linalg.eigh(cov)    if np.any(eigenvalues <= 0):        return    order = np.argsort(eigenvalues)[::-1]    eigenvalues = eigenvalues[order]    eigenvectors = eigenvectors[:, order]    angle = np.degrees(np.arctan2(eigenvectors[10], eigenvectors[00]))    radius = math.sqrt(chi2.ppf(level, df=2))    width, height = 2 * radius * np.sqrt(eigenvalues)    ellipse = Ellipse(        xy=(np.mean(x), np.mean(y)),        width=width,        height=height,        angle=angle,        facecolor=mpl.colors.to_rgba(color, 0.10),        edgecolor=color,        linewidth=1.2,        linestyle="--",        zorder=1,    )    ax.add_patch(ellipse)def calculate_pca(    expr: pd.DataFrame,    metadata: pd.DataFrame,    top_variable_genes: int = 5000,) -> Tuple[pd.DataFrame, np.ndarray]:    variances = expr.var(axis=1).sort_values(ascending=False)    selected = variances.head(min(top_variable_genes, len(variances))).index    matrix = expr.loc[selected].T    # Center genes but do not scale each gene to unit variance by default.    centered = StandardScaler(with_mean=True, with_std=False).fit_transform(matrix)    pca = PCA(n_components=2, random_state=0)    scores = pca.fit_transform(centered)    explained = pca.explained_variance_ratio_ * 100    result = metadata.copy()    result["PC1"] = scores[:, 0]    result["PC2"] = scores[:, 1]    return result, explaineddef plot_pca(    ax: mpl.axes.Axes,    expr: pd.DataFrame,    metadata: pd.DataFrame,    title: str,    project_palette: Dict[strstr],    group_markers: Dict[strstr],    top_variable_genes: int = 5000,    show_legend: bool = True,) -> pd.DataFrame:    pca_df, explained = calculate_pca(expr, metadata, top_variable_genes)    for project in pca_df["Project"].drop_duplicates():        project_df = pca_df[pca_df["Project"] == project]        for group in project_df["Group"].drop_duplicates():            subset = project_df[project_df["Group"] == group]            ax.scatter(                subset["PC1"],                subset["PC2"],                s=42,                c=project_palette[project],                marker=group_markers[group],                edgecolors="black",                linewidths=0.55,                alpha=0.90,                zorder=3,            )        add_confidence_ellipse(            ax,            project_df["PC1"].to_numpy(),            project_df["PC2"].to_numpy(),            color=project_palette[project],            level=0.95,        )    ax.axhline(0, color="#D9D9D9", linewidth=0.7, zorder=0)    ax.axvline(0, color="#D9D9D9", linewidth=0.7, zorder=0)    ax.set_xlabel(f"PC1 ({explained[0]:.2f}%)")    ax.set_ylabel(f"PC2 ({explained[1]:.2f}%)")    ax.set_title(title, fontsize=12, pad=8)    ax.grid(False)    if show_legend:        project_handles = [            Line2D(                [0], [0], marker="o", linestyle="none", markersize=6,                markerfacecolor=project_palette[p], markeredgecolor="black",                markeredgewidth=0.5, label=p,            )            for p in project_palette        ]        group_handles = [            Line2D(                [0], [0], marker=group_markers[g], linestyle="none", markersize=6,                markerfacecolor="white", markeredgecolor="black", label=g,            )            for g in group_markers        ]        first = ax.legend(            handles=project_handles,            title="Project",            loc="upper left",            bbox_to_anchor=(1.021.00),            borderaxespad=0,            fontsize=8,            title_fontsize=9,        )        ax.add_artist(first)        ax.legend(            handles=group_handles,            title="Group",            loc="lower left",            bbox_to_anchor=(1.020.00),            borderaxespad=0,            fontsize=8,            title_fontsize=9,        )    return pca_df# =============================================================================# QC boxplots# =============================================================================def plot_sample_boxplot(    ax: mpl.axes.Axes,    expr: pd.DataFrame,    metadata: pd.DataFrame,    title: str,    project_palette: Dict[strstr],    show_every_n: Optional[int] = None,    show_legend: bool = True,) -> None:    sample_order = metadata.index.tolist()    values = [expr[sample].dropna().to_numpy() for sample in sample_order]    colors = [project_palette[metadata.loc[sample, "Project"]] for sample in sample_order]    box = ax.boxplot(        values,        patch_artist=True,        notch=False,        showfliers=False,        widths=0.55,        medianprops={"color""black""linewidth"1.0},        whiskerprops={"color""black""linewidth"0.8},        capprops={"color""black""linewidth"0.8},        boxprops={"edgecolor""black""linewidth"0.8},    )    for patch, color in zip(box["boxes"], colors):        patch.set_facecolor(color)        patch.set_alpha(0.82)    positions, labels = sparse_xticks(metadata["Sample"].tolist(), show_every_n)    ax.set_xticks(positions)    ax.set_xticklabels(labels, rotation=45, ha="right", fontsize=7)    ax.set_xlabel("Sample")    ax.set_ylabel("Expression")    ax.set_title(title, fontsize=12, pad=8)    ax.grid(False)    if show_legend:        handles = [            Patch(facecolor=project_palette[p], edgecolor="black", linewidth=0.6, label=p)            for p in project_palette        ]        ax.legend(            handles=handles,            title="Project",            loc="upper left",            bbox_to_anchor=(1.021.00),            borderaxespad=0,            fontsize=8,            title_fontsize=9,        )# =============================================================================# Differential-expression analysis# =============================================================================def prepare_two_group_data(    expr: pd.DataFrame,    metadata: pd.DataFrame,    control: str,    treat: str,) -> Tuple[pd.DataFrame, pd.DataFrame, strstr]:    control_resolved = resolve_label(metadata["Group"], control)    treat_resolved = resolve_label(metadata["Group"], treat)    if control_resolved == treat_resolved:        raise ValueError("Control and treatment groups must be different.")    keep = metadata["Group"].isin([control_resolved, treat_resolved])    excluded = metadata.loc[~keep, "Group"].drop_duplicates().tolist()    if excluded:        warnings.warn(            "The following groups are excluded from two-group differential analysis: "            + ", ".join(excluded),            RuntimeWarning,        )    meta = metadata.loc[keep].copy()    expression = expr.loc[:, meta.index].copy()    return expression, meta, control_resolved, treat_resolveddef differential_expression_ols(    expr: pd.DataFrame,    metadata: pd.DataFrame,    control: str,    treat: str,) -> Tuple[pd.DataFrame, pd.DataFrame]:    """    Per-gene OLS model:        expression ~ intercept + treatment + project covariates    The treatment coefficient is the log2 fold change (Treat - Control) when the    expression matrix is on the log2 scale.    """    expr, meta, control, treat = prepare_two_group_data(expr, metadata, control, treat)    group_counts = meta["Group"].value_counts()    if group_counts.get(control, 0) < 2 or group_counts.get(treat, 0) < 2:        raise ValueError(            f"At least two samples are required in each group. Counts: {group_counts.to_dict()}"        )    design = pd.DataFrame(index=meta.index)    design["Intercept"] = 1.0    design[f"{treat}_vs_{control}"] = (meta["Group"] == treat).astype(float)    project_dummies = pd.get_dummies(        meta["Project"], prefix="Project", drop_first=True, dtype=float    )    design = pd.concat([design, project_dummies], axis=1)    x = design.to_numpy(dtype=float)    rank = np.linalg.matrix_rank(x)    if rank < x.shape[1]:        cross_tab = pd.crosstab(meta["Project"], meta["Group"])        raise ValueError(            "The design matrix is rank deficient. Group and project may be fully "            "confounded, so the treatment effect cannot be separated from batch.\n"            f"Project x Group table:\n{cross_tab.to_string()}"        )    n_samples, n_parameters = x.shape    df_residual = n_samples - n_parameters    if df_residual <= 0:        raise ValueError(            f"Insufficient residual degrees of freedom: samples={n_samples}, "            f"parameters={n_parameters}."        )    y = expr.T.to_numpy(dtype=float)  # samples x genes    xtx_inv = np.linalg.inv(x.T @ x)    beta = xtx_inv @ x.T @ y    residuals = y - x @ beta    sigma2 = np.sum(residuals**2, axis=0) / df_residual    treatment_index = 1    standard_error = np.sqrt(np.maximum(sigma2, 0) * xtx_inv[treatment_index, treatment_index])    logfc = beta[treatment_index, :]    with np.errstate(divide="ignore", invalid="ignore"):        t_stat = logfc / standard_error    p_values = 2 * student_t.sf(np.abs(t_stat), df=df_residual)    p_values = np.nan_to_num(p_values, nan=1.0, posinf=0.0, neginf=0.0)    adjusted = multipletests(p_values, alpha=0.05, method="fdr_bh")[1]    result = pd.DataFrame(        {            "Gene": expr.index,            "logFC": logfc,            "AveExpr": np.mean(y, axis=0),            "SE": standard_error,            "t": t_stat,            "P.Value": p_values,            "adj.P.Val": adjusted,            "df_residual": df_residual,        }    ).set_index("Gene")    result = result.sort_values(["adj.P.Val""P.Value""logFC"], ascending=[TrueTrueFalse])    return result, designdef classify_degs(    result: pd.DataFrame,    logfc_cutoff: float,    fdr_cutoff: float,) -> pd.DataFrame:    classified = result.copy()    classified["Significance"] = "Not significant"    up_mask = (classified["logFC"] > logfc_cutoff) & (        classified["adj.P.Val"] < fdr_cutoff    )    down_mask = (classified["logFC"] < -logfc_cutoff) & (        classified["adj.P.Val"] < fdr_cutoff    )    classified.loc[up_mask, "Significance"] = "Up-regulated"    classified.loc[down_mask, "Significance"] = "Down-regulated"    return classified# =============================================================================# DEG visualizations# =============================================================================def _scatter_by_significance(    ax: mpl.axes.Axes,    data: pd.DataFrame,    x: str,    y: str,    point_size: float = 11,) -> None:    order = ["Not significant""Down-regulated""Up-regulated"]    for category in order:        subset = data[data["Significance"] == category]        ax.scatter(            subset[x],            subset[y],            s=point_size,            color=REGULATION_COLORS[category],            alpha=0.70 if category != "Not significant" else 0.38,            edgecolors="none",            rasterized=True,            label=category,        )def plot_volcano(    ax: mpl.axes.Axes,    result: pd.DataFrame,    logfc_cutoff: float,    fdr_cutoff: float,    top_labels_each: int = 10,    show_legend: bool = True,) -> None:    plot_df = result.copy()    tiny = np.finfo(float).tiny    plot_df["minus_log10_fdr"] = -np.log10(plot_df["adj.P.Val"].clip(lower=tiny))    _scatter_by_significance(ax, plot_df, "logFC""minus_log10_fdr", point_size=12)    ax.axvline(-logfc_cutoff, linestyle="--", color="#666666", linewidth=0.8)    ax.axvline(logfc_cutoff, linestyle="--", color="#666666", linewidth=0.8)    ax.axhline(-math.log10(fdr_cutoff), linestyle="--", color="#666666", linewidth=0.8)    up = plot_df[plot_df["Significance"] == "Up-regulated"].nsmallest(        top_labels_each, "adj.P.Val"    )    down = plot_df[plot_df["Significance"] == "Down-regulated"].nsmallest(        top_labels_each, "adj.P.Val"    )    labels = pd.concat([up, down])    texts = []    for gene, row in labels.iterrows():        texts.append(            ax.text(                row["logFC"],                row["minus_log10_fdr"],                str(gene),                fontsize=7,                ha="center",                va="bottom",            )        )    if adjust_text is not None and texts:        adjust_text(            texts,            ax=ax,            arrowprops={"arrowstyle""-""color""#808080""lw"0.5},            expand_points=(1.21.3),            expand_text=(1.11.2),            force_text=(0.20.3),        )    counts = plot_df["Significance"].value_counts()    ax.text(        0.02,        0.98,        f"Down: {counts.get('Down-regulated'0):,}\n"        f"Up: {counts.get('Up-regulated'0):,}",        transform=ax.transAxes,        ha="left",        va="top",        fontsize=8,        fontweight="bold",    )    ax.set_xlabel(r"$\log_2$ fold change")    ax.set_ylabel(r"$-\log_{10}$ adjusted P-value")    ax.set_title("Volcano plot", fontsize=12, pad=8)    ax.grid(False)    if show_legend:        ax.legend(            title="Expression",            loc="upper left",            bbox_to_anchor=(1.021.00),            borderaxespad=0,            fontsize=8,            title_fontsize=9,            markerscale=1.2,        )def plot_ma(    ax: mpl.axes.Axes,    result: pd.DataFrame,    logfc_cutoff: float,    show_legend: bool = True,) -> None:    _scatter_by_significance(ax, result, "AveExpr""logFC", point_size=12)    ax.axhline(0, color="black", linewidth=0.8)    ax.axhline(logfc_cutoff, linestyle="--", color="#666666", linewidth=0.8)    ax.axhline(-logfc_cutoff, linestyle="--", color="#666666", linewidth=0.8)    ax.set_xlabel("Average expression")    ax.set_ylabel(r"$\log_2$ fold change")    ax.set_title("MA plot", fontsize=12, pad=8)    ax.grid(False)    if show_legend:        ax.legend(            title="Expression",            loc="upper left",            bbox_to_anchor=(1.021.00),            borderaxespad=0,            fontsize=8,            title_fontsize=9,            markerscale=1.2,        )def plot_deg_summary(ax: mpl.axes.Axes, result: pd.DataFrame) -> None:    counts = result["Significance"].value_counts()    categories = ["Down-regulated""Up-regulated"]    values = [counts.get(cat, 0for cat in categories]    bars = ax.bar(        ["Down""Up"],        values,        color=[REGULATION_COLORS[c] for c in categories],        width=0.62,        edgecolor="black",        linewidth=0.6,    )    ymax = max(values) if max(values, default=0) > 0 else 1    for bar, value in zip(bars, values):        ax.text(            bar.get_x() + bar.get_width() / 2,            value + ymax * 0.03,            f"{value:,}",            ha="center",            va="bottom",            fontsize=9,            fontweight="bold",        )    ax.set_ylim(0, ymax * 1.18)    ax.set_ylabel("Number of genes")    ax.set_title("Significant DEGs", fontsize=12, pad=8)    ax.grid(axis="y", color="#E6E6E6", linewidth=0.6)    ax.set_axisbelow(True)def plot_pvalue_distribution(ax: mpl.axes.Axes, result: pd.DataFrame) -> None:    ax.hist(        result["P.Value"].dropna(),        bins=50,        color="#4DBBD5",        edgecolor="white",        linewidth=0.35,        alpha=0.90,    )    ax.axvline(0.05, linestyle="--", color="#E64B35", linewidth=1.0)    ax.set_xlabel("Raw P-value")    ax.set_ylabel("Frequency")    ax.set_title("P-value distribution", fontsize=12, pad=8)    ax.grid(False)def select_top_degs(    result: pd.DataFrame,    each_direction: int,) -> Tuple[List[str], List[str]]:    up = result[result["Significance"] == "Up-regulated"].nsmallest(        each_direction, "adj.P.Val"    )    down = result[result["Significance"] == "Down-regulated"].nsmallest(        each_direction, "adj.P.Val"    )    return up.index.tolist(), down.index.tolist()def plot_heatmap(    expr: pd.DataFrame,    metadata: pd.DataFrame,    result: pd.DataFrame,    output_stem: Path,    formats: Sequence[str],    raster_dpi: int,    top_each: int,    group_palette: Dict[strstr],    project_palette: Dict[strstr],) -> None:    up_genes, down_genes = select_top_degs(result, top_each)    genes = up_genes + down_genes    if not genes:        warnings.warn("No significant genes available for heatmap; heatmap skipped.")        return    heat = expr.loc[genes, metadata.index].copy()    row_std = heat.std(axis=1, ddof=0).replace(0, np.nan)    heat_z = heat.sub(heat.mean(axis=1), axis=0).div(row_std, axis=0).fillna(0)    col_colors = pd.DataFrame(        {            "Group": metadata["Group"].map(group_palette),            "Project": metadata["Project"].map(project_palette),        },        index=metadata.index,    )    height = max(7.0min(14.03.5 + 0.075 * len(genes)))    width = max(9.0min(16.05.5 + 0.055 * heat_z.shape[1]))    grid = sns.clustermap(        heat_z,        cmap=sns.diverging_palette(24010, as_cmap=True),        center=0,        row_cluster=(len(genes) > 1),        col_cluster=False,        col_colors=col_colors,        xticklabels=False,        yticklabels=True,        linewidths=0,        figsize=(width, height),        cbar_kws={"label""Row Z-score"},        dendrogram_ratio=(0.140.04),        colors_ratio=(0.030.03),    )    grid.ax_heatmap.set_xlabel("Samples")    grid.ax_heatmap.set_ylabel("Genes")    grid.ax_heatmap.tick_params(axis="y", labelsize=6)    grid.fig.suptitle(        "Top differentially expressed genes",        fontsize=14,        fontweight="bold",        y=1.01,    )    legend_handles = [        Patch(facecolor=group_palette[g], edgecolor="none", label=f"Group: {g}")        for g in group_palette    ] + [        Patch(facecolor=project_palette[p], edgecolor="none", label=f"Project: {p}")        for p in project_palette    ]    grid.fig.legend(        handles=legend_handles,        loc="center left",        bbox_to_anchor=(1.0050.5),        fontsize=7,        frameon=False,    )    for fmt in formats:        fmt = fmt.lower().lstrip(".")        kwargs = {"bbox_inches""tight""facecolor""white"}        if fmt in {"png""tif""tiff""jpg""jpeg"}:            kwargs["dpi"] = raster_dpi        grid.savefig(output_stem.with_suffix(f".{fmt}"), **kwargs)    plt.close(grid.fig)def plot_top_gene_boxplots(    expr: pd.DataFrame,    metadata: pd.DataFrame,    result: pd.DataFrame,    output_stem: Path,    formats: Sequence[str],    raster_dpi: int,    top_each: int,    group_palette: Dict[strstr],    seed: int,) -> None:    up_genes, down_genes = select_top_degs(result, top_each)    genes = up_genes + down_genes    if not genes:        warnings.warn("No significant genes available for top-gene boxplots; skipped.")        return    long_df = (        expr.loc[genes, metadata.index]        .T        .join(metadata[["Group""Project"]])        .reset_index(names="Original")        .melt(            id_vars=["Original""Group""Project"],            value_vars=genes,            var_name="Gene",            value_name="Expression",        )    )    ncols = 3    nrows = math.ceil(len(genes) / ncols)    fig, axes = plt.subplots(        nrows,        ncols,        figsize=(3.2 * ncols, 2.75 * nrows),        squeeze=False,    )    rng = np.random.default_rng(seed)    group_order = list(group_palette)    for ax, gene in zip(axes.flat, genes):        subset = long_df[long_df["Gene"] == gene]        sns.boxplot(            data=subset,            x="Group",            y="Expression",            hue="Group",            order=group_order,            hue_order=group_order,            palette=group_palette,            dodge=False,            legend=False,            width=0.58,            linewidth=0.9,            fliersize=0,            ax=ax,        )        # Deterministic jitter, avoiding seaborn's version-dependent random state.        for i, group in enumerate(group_order):            vals = subset.loc[subset["Group"] == group, "Expression"].to_numpy()            jitter = rng.normal(loc=i, scale=0.045, size=len(vals))            ax.scatter(                jitter,                vals,                s=18,                color=group_palette[group],                edgecolors="black",                linewidths=0.4,                alpha=0.75,                zorder=3,            )        fc = result.loc[gene, "logFC"]        fdr = result.loc[gene, "adj.P.Val"]        ax.set_title(f"{gene}\nlogFC={fc:.2f}, FDR={fdr:.2g}", fontsize=9, pad=5)        ax.set_xlabel("")        ax.set_ylabel("Expression")        ax.tick_params(axis="x", rotation=20, labelsize=8)        ax.grid(axis="y", color="#ECECEC", linewidth=0.5)        ax.set_axisbelow(True)    for ax in axes.flat[len(genes) :]:        ax.axis("off")    fig.suptitle("Expression of top differentially expressed genes", fontsize=14, y=1.01)    fig.tight_layout()    save_figure(fig, output_stem, formats, raster_dpi)# =============================================================================# Output tables and report# =============================================================================def save_tables(    expr: pd.DataFrame,    metadata: pd.DataFrame,    result: pd.DataFrame,    design: pd.DataFrame,    result_dir: Path,    logfc_cutoff: float,    fdr_cutoff: float,    control: str,    treat: str,) -> Dict[strint]:    significant = result[        (result["adj.P.Val"] < fdr_cutoff) & (result["logFC"].abs() > logfc_cutoff)    ].copy()    up = significant[significant["logFC"] > 0]    down = significant[significant["logFC"] < 0]    result.to_csv(result_dir / "all_genes.tsv", sep="\t", index=True)    significant.to_csv(result_dir / "significant_genes.tsv", sep="\t", index=True)    up.to_csv(result_dir / "upregulated_genes.tsv", sep="\t", index=True)    down.to_csv(result_dir / "downregulated_genes.tsv", sep="\t", index=True)    metadata.to_csv(result_dir / "sample_metadata.tsv", sep="\t", index=False)    design.to_csv(result_dir / "design_matrix.tsv", sep="\t", index=True)    # Preserve project identifiers and normalize only the terminal group suffix.    renamed_columns = []    for original in expr.columns:        group = metadata.loc[original, "Group"]        suffix = "con" if group == control else "tre" if group == treat else group        base = original.rsplit("_"1)[0]        renamed_columns.append(f"{base}_{suffix}")    sig_matrix = expr.loc[significant.index].copy()    sig_matrix.columns = renamed_columns    sig_matrix.insert(0"GeneName", sig_matrix.index)    sig_matrix.to_csv(result_dir / "Sample_Type_Matrix.csv", index=False)    all_matrix = expr.copy()    all_matrix.columns = renamed_columns    all_matrix.insert(0"GeneName", all_matrix.index)    all_matrix.to_csv(result_dir / "All_Gene_Sample_Type_Matrix.csv", index=False)    return {        "total_genes"int(len(result)),        "significant_genes"int(len(significant)),        "upregulated_genes"int(len(up)),        "downregulated_genes"int(len(down)),    }def write_report(    path: Path,    config: PipelineConfig,    counts: Dict[strint],    input_info: Dict[strobject],    design: pd.DataFrame,) -> None:    report = {        "analysis_method": (            "Per-gene ordinary least squares: expression ~ treatment + project covariates; "            "Benjamini-Hochberg FDR correction"        ),        "contrast"f"{config.treat} - {config.control}",        "log2_fold_change_cutoff": config.logfc_cutoff,        "fdr_cutoff": config.fdr_cutoff,        "counts": counts,        "input": input_info,        "design_columns": design.columns.tolist(),        "note": (            "This pure-Python model is not the empirical-Bayes moderated t-test used by limma. "            "For an exact limma analysis, run limma in R or call R from Python."        ),    }    path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")# =============================================================================# Pipeline orchestration# =============================================================================def run_pipeline(config: PipelineConfig) -> None:    np.random.seed(config.seed)    configure_publication_style()    paths = ensure_dirs(config.outdir)    print("[1/7] Reading normalized expression matrix...")    norm_expr, norm_meta, norm_info = read_expression(        config.norm_file,        auto_log2=config.auto_log2,        max_missing_fraction=config.max_missing_fraction,    )    pre_expr = pre_meta = None    pre_info = None    if config.pre_file is not None:        print("[2/7] Reading pre-correction expression matrix...")        pre_expr, pre_meta, pre_info = read_expression(            config.pre_file,            auto_log2=config.auto_log2,            max_missing_fraction=config.max_missing_fraction,        )    else:        print("[2/7] Pre-correction matrix not supplied; before/after QC comparison skipped.")    all_projects = norm_meta["Project"].tolist()    all_groups = norm_meta["Group"].tolist()    if pre_meta is not None:        all_projects += pre_meta["Project"].tolist()        all_groups += pre_meta["Group"].tolist()    project_palette = make_palette(all_projects)    group_palette = make_palette(all_groups)    group_markers = make_marker_map(all_groups)    print("[3/7] Creating sample-distribution QC figures...")    if pre_expr is not None and pre_meta is not None:        fig, axes = plt.subplots(12, figsize=(14.05.5))        plot_sample_boxplot(            axes[0], pre_expr, pre_meta, "Before batch correction",            project_palette, config.show_every_n, show_legend=False,        )        plot_sample_boxplot(            axes[1], norm_expr, norm_meta, "After batch correction",            project_palette, config.show_every_n, show_legend=True,        )        axes[0].text(-0.081.04"A", transform=axes[0].transAxes, fontsize=14, fontweight="bold")        axes[1].text(-0.081.04"B", transform=axes[1].transAxes, fontsize=14, fontweight="bold")        fig.tight_layout(rect=[000.891])        save_figure(            fig,            paths["figures"] / "Figure_1_batch_correction_boxplots",            config.formats,            config.raster_dpi,        )    else:        fig, ax = plt.subplots(figsize=(11.05.5))        plot_sample_boxplot(            ax, norm_expr, norm_meta, "Normalized expression distributions",            project_palette, config.show_every_n, show_legend=True,        )        fig.tight_layout(rect=[000.861])        save_figure(            fig,            paths["figures"] / "Figure_1_expression_boxplots",            config.formats,            config.raster_dpi,        )    print("[4/7] Creating PCA figures...")    if pre_expr is not None and pre_meta is not None:        fig, axes = plt.subplots(12, figsize=(12.55.2))        plot_pca(            axes[0], pre_expr, pre_meta, "Before batch correction",            project_palette, group_markers, config.pca_top_variable_genes,            show_legend=False,        )        plot_pca(            axes[1], norm_expr, norm_meta, "After batch correction",            project_palette, group_markers, config.pca_top_variable_genes,            show_legend=True,        )        axes[0].text(-0.141.04"A", transform=axes[0].transAxes, fontsize=14, fontweight="bold")        axes[1].text(-0.141.04"B", transform=axes[1].transAxes, fontsize=14, fontweight="bold")        fig.tight_layout(rect=[000.851])        save_figure(            fig,            paths["figures"] / "Figure_2_batch_correction_PCA",            config.formats,            config.raster_dpi,        )    else:        fig, ax = plt.subplots(figsize=(6.65.4))        plot_pca(            ax, norm_expr, norm_meta, "PCA of normalized samples",            project_palette, group_markers, config.pca_top_variable_genes,            show_legend=True,        )        fig.tight_layout(rect=[000.801])        save_figure(            fig,            paths["figures"] / "Figure_2_PCA",            config.formats,            config.raster_dpi,        )    print("[5/7] Running differential-expression analysis...")    de_result, design = differential_expression_ols(        norm_expr,        norm_meta,        config.control,        config.treat,    )    control_resolved = resolve_label(norm_meta["Group"], config.control)    treat_resolved = resolve_label(norm_meta["Group"], config.treat)    de_result = classify_degs(de_result, config.logfc_cutoff, config.fdr_cutoff)    counts = save_tables(        norm_expr,        norm_meta,        de_result,        design,        paths["results"],        config.logfc_cutoff,        config.fdr_cutoff,        control_resolved,        treat_resolved,    )    print("[6/7] Creating differential-expression figures...")    fig, axes = plt.subplots(22, figsize=(11.89.4))    plot_volcano(        axes[00], de_result, config.logfc_cutoff, config.fdr_cutoff,        config.top_labels_each, show_legend=False,    )    plot_ma(axes[01], de_result, config.logfc_cutoff, show_legend=True)    plot_deg_summary(axes[10], de_result)    plot_pvalue_distribution(axes[11], de_result)    for label, ax in zip(["A""B""C""D"], axes.flat):        ax.text(-0.121.06, label, transform=ax.transAxes, fontsize=14, fontweight="bold")    fig.suptitle("Differential expression overview", fontsize=15, fontweight="bold", y=1.01)    fig.tight_layout(rect=[000.900.99])    save_figure(        fig,        paths["figures"] / "Figure_3_differential_expression_overview",        config.formats,        config.raster_dpi,    )    # Individual volcano plot, often needed as a standalone main figure.    fig, ax = plt.subplots(figsize=(7.25.8))    plot_volcano(        ax, de_result, config.logfc_cutoff, config.fdr_cutoff,        config.top_labels_each, show_legend=True,    )    fig.tight_layout(rect=[000.821])    save_figure(        fig,        paths["figures"] / "Figure_3A_volcano_plot",        config.formats,        config.raster_dpi,    )    # Heatmap and top-gene boxplots use only the two modeled groups.    expr_two, meta_two, _, _ = prepare_two_group_data(        norm_expr, norm_meta, config.control, config.treat    )    modeled_group_palette = {        g: group_palette[g] for g in meta_two["Group"].drop_duplicates()    }    modeled_project_palette = {        p: project_palette[p] for p in meta_two["Project"].drop_duplicates()    }    plot_heatmap(        expr_two,        meta_two,        de_result,        paths["figures"] / "Figure_4_top_DEG_heatmap",        config.formats,        config.raster_dpi,        config.top_heatmap_each,        modeled_group_palette,        modeled_project_palette,    )    plot_top_gene_boxplots(        expr_two,        meta_two,        de_result,        paths["figures"] / "Figure_5_top_gene_boxplots",        config.formats,        config.raster_dpi,        config.top_box_each,        modeled_group_palette,        config.seed,    )    print("[7/7] Writing analysis report...")    input_info = {"normalized": norm_info, "pre_correction": pre_info}    write_report(        paths["results"] / "analysis_report.json",        config,        counts,        input_info,        design,    )    print("\nAnalysis complete")    print(f"Output directory: {config.outdir.resolve()}")    print(f"Total genes tested: {counts['total_genes']:,}")    print(f"Significant DEGs: {counts['significant_genes']:,}")    print(f"Up-regulated: {counts['upregulated_genes']:,}")    print(f"Down-regulated: {counts['downregulated_genes']:,}")# =============================================================================# Command-line interface# =============================================================================def parse_args(argv: Optional[Sequence[str]] = None) -> PipelineConfig:    parser = argparse.ArgumentParser(        description="Integrated SCI-style transcriptomic QC and DEG analysis pipeline."    )    parser.add_argument(        "--norm",        required=True,        type=Path,        help="Normalized/batch-corrected expression matrix (genes x samples, TSV).",    )    parser.add_argument(        "--pre",        type=Path,        default=None,        help="Optional pre-correction expression matrix for before/after QC.",    )    parser.add_argument(        "--outdir",        type=Path,        default=Path("sci_deg_output"),        help="Output directory. Default: sci_deg_output",    )    parser.add_argument("--control", default="Control"help="Control group label.")    parser.add_argument("--treat", default="Treat"help="Treatment group label.")    parser.add_argument(        "--logfc"type=float, default=0.585help="Absolute log2FC cutoff."    )    parser.add_argument("--fdr"type=float, default=0.05help="FDR cutoff.")    parser.add_argument(        "--top-heatmap-each",        type=int,        default=50,        help="Top up and down genes shown in the heatmap.",    )    parser.add_argument(        "--top-labels-each",        type=int,        default=10,        help="Top up and down genes labeled on the volcano plot.",    )    parser.add_argument(        "--top-box-each",        type=int,        default=3,        help="Top up and down genes shown in expression boxplots.",    )    parser.add_argument(        "--pca-top-genes",        type=int,        default=5000,        help="Number of most-variable genes used for PCA.",    )    parser.add_argument(        "--show-every-n",        type=int,        default=None,        help="Display every Nth sample label in expression boxplots.",    )    parser.add_argument(        "--no-auto-log2",        action="store_true",        help="Disable conservative automatic log2 transformation detection.",    )    parser.add_argument(        "--max-missing-fraction",        type=float,        default=0.20,        help="Remove genes with a larger missing-value fraction.",    )    parser.add_argument(        "--formats",        nargs="+",        default=["pdf""png"],        choices=["pdf""png""tif""tiff""svg"],        help="Figure formats. Example: --formats pdf tiff",    )    parser.add_argument(        "--dpi"type=int, default=600help="DPI for raster figure formats."    )    parser.add_argument("--seed"type=int, default=2026help="Random seed.")    args = parser.parse_args(argv)    if args.logfc < 0:        parser.error("--logfc must be non-negative")    if not (0 < args.fdr < 1):        parser.error("--fdr must be between 0 and 1")    if not (0 <= args.max_missing_fraction < 1):        parser.error("--max-missing-fraction must be in [0, 1)")    return PipelineConfig(        pre_file=args.pre,        norm_file=args.norm,        outdir=args.outdir,        control=args.control,        treat=args.treat,        logfc_cutoff=args.logfc,        fdr_cutoff=args.fdr,        top_heatmap_each=args.top_heatmap_each,        top_labels_each=args.top_labels_each,        top_box_each=args.top_box_each,        pca_top_variable_genes=args.pca_top_genes,        show_every_n=args.show_every_n,        auto_log2=not args.no_auto_log2,        max_missing_fraction=args.max_missing_fraction,        formats=tuple(args.formats),        raster_dpi=args.dpi,        seed=args.seed,    )def main(argv: Optional[Sequence[str]] = None) -> int:    try:        config = parse_args(argv)        run_pipeline(config)        return 0    except Exception as exc:        print(f"ERROR: {exc}", file=sys.stderr)        return 1if __name__ == "__main__":    raise SystemExit(main())

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-21 20:53:27 HTTP/2.0 GET : https://f.mffb.com.cn/a/507292.html
  2. 运行时间 : 0.491432s [ 吞吐率:2.03req/s ] 内存消耗:4,903.36kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=b19bf57b13e97304dba69b2ceff1ae28
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000874s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001272s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.012605s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000708s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001631s ]
  6. SELECT * FROM `set` [ RunTime:0.000598s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001691s ]
  8. SELECT * FROM `article` WHERE `id` = 507292 LIMIT 1 [ RunTime:0.003242s ]
  9. UPDATE `article` SET `lasttime` = 1787316807 WHERE `id` = 507292 [ RunTime:0.022826s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000713s ]
  11. SELECT * FROM `article` WHERE `id` < 507292 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.010710s ]
  12. SELECT * FROM `article` WHERE `id` > 507292 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.012860s ]
  13. SELECT * FROM `article` WHERE `id` < 507292 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.076477s ]
  14. SELECT * FROM `article` WHERE `id` < 507292 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.068590s ]
  15. SELECT * FROM `article` WHERE `id` < 507292 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.093925s ]
0.495101s