c14_pca_umap 高维聚类 (Clustering) Nature Cell Lancet

PCA / t-SNE / UMAP 2D Projection with Confidence Ellipses

单细胞/高维特征 PCA/t-SNE/UMAP 2D 投影图,包含各群别 95% 协方差置信椭圆与群质心 (Centroid) 标注。

🔬 矢量预览 (Vector Preview) 600 DPI Ready
PCA / t-SNE / UMAP 2D Projection with Confidence Ellipses

📊 统计学特性 (Statistical Features)

  • 二维投影坐标映射
  • 95% 协方差置信椭圆 (Eigenvalues)
  • 群别中心点 (Centroid) 计算
  • 类别离散度量化

🛡️ QC 质检要点 (QC Highlights)

  • 置信椭圆边缘清晰半透明填充
  • 质心文字标签醒目
  • 海量点渲染性能与透明度
  • 坐标轴注明方差解释率
🎨 顶刊色彩提取器 (Palette Extractor)Nature

Crisp sans-serif typography, clean borderless spines, high-contrast palette with soft muted secondary accents.

🛡️ 无障碍评分:AAA (Deuteranopia & Protanopia Compliant)

📋 Data Contract 数据契约规范 Strict Schema

输入数据必须完全符合下列列名与数据类型规范,方可通过自动化数据前检 (Pre-flight Validation)。

📥 下载规范示例 CSV 数据
字段名称 (Column) 数据类型 (Type) 约束性 语义说明与值域约束
dim_1 continuous 必需 (Required) First projection coordinate (e.g. PC1, UMAP_1)
dim_2 continuous 必需 (Required) Second projection coordinate (e.g. PC2, UMAP_2)
cluster categorical 必需 (Required) Cluster, cell-type, or classification label
sample_id string 可选 (Optional) Specimen or single-cell barcode ID
组内最小样本量: n ≥ 5
最大允许缺失率: 5%

🐍 独立可复现 Python 绘图源码 Stand-alone Script

完全可复现的 Python 脚本,支持 CLI 参数 `--data`, `--style`, `--output-dir` 与模块化 `render()` 调用。

#!/usr/bin/env python3
"""FigureCraft Chart Engine: PCA / UMAP 2D Projection with Confidence Ellipses (c14_pca_umap)."""

import os
import sys
import argparse
from pathlib import Path
from typing import List, Dict, Optional
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as patches

try:
    from engines.styles import apply_style, get_palette
except ImportError:
    sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
    from engines.styles import apply_style, get_palette

CHART_ID = "c14_pca_umap"


def compute_confidence_ellipse(x: np.ndarray, y: np.ndarray, confidence: float = 0.95) -> Optional[patches.Ellipse]:
    """Calculates theoretical 95% confidence ellipse from bivariate sample covariance."""
    if len(x) < 3:
        return None

    cov = np.cov(x, y)
    vals, vecs = np.linalg.eigh(cov)
    order = vals.argsort()[::-1]
    vals = vals[order]
    vecs = vecs[:, order]

    # Chi-square critical value: df=2, 0.95 -> 5.991
    # For general confidence: -2 * log(1 - confidence)
    s = -2.0 * np.log(1.0 - confidence)
    width = 2.0 * np.sqrt(s * np.maximum(vals[0], 1e-12))
    height = 2.0 * np.sqrt(s * np.maximum(vals[1], 1e-12))

    theta = np.degrees(np.arctan2(vecs[1, 0], vecs[0, 0]))
    center = (np.mean(x), np.mean(y))

    return patches.Ellipse(xy=center, width=width, height=height, angle=theta)


def render(
    data_path: Optional[str] = None,
    style: str = "nature",
    output_dir: str = "output",
    formats: Optional[List[str]] = None,
    dpi: int = 600,
) -> Dict[str, str]:
    if formats is None:
        formats = ["svg", "pdf", "png", "tiff"]
    if data_path is None:
        data_path = str(Path(__file__).resolve().parents[1] / "data" / f"{CHART_ID}.csv")

    apply_style(style)
    palette = get_palette(style)
    df = pd.read_csv(data_path)
    df = df.dropna(subset=["dim_1", "dim_2", "cluster"])

    clusters = list(df["cluster"].unique())

    fig, ax = plt.subplots(figsize=(6.5, 5.2))

    for i, clus in enumerate(clusters):
        sub_df = df[df["cluster"] == clus]
        x = sub_df["dim_1"].values
        y = sub_df["dim_2"].values
        color = palette[i % len(palette)]
        clus_label = clus.replace("_", " ")

        # Scatter points
        ax.scatter(
            x,
            y,
            c=color,
            label=clus_label,
            s=34,
            alpha=0.8,
            edgecolors="#FFFFFF",
            linewidth=0.5,
            zorder=3,
        )

        # 95% Confidence Ellipse
        ellipse = compute_confidence_ellipse(x, y, confidence=0.95)
        if ellipse:
            ellipse.set_facecolor(color)
            ellipse.set_alpha(0.12)
            ellipse.set_edgecolor(color)
            ellipse.set_linewidth(1.2)
            ellipse.set_linestyle("-")
            ellipse.set_zorder(2)
            ax.add_patch(ellipse)

        # Cluster Centroid label
        cx, cy = np.mean(x), np.mean(y)
        ax.scatter(cx, cy, color=color, s=70, marker="X", edgecolors="#111827", linewidth=0.8, zorder=5)

    ax.set_xlabel("Principal Coordinate 1 (38.4% variance explained)")
    ax.set_ylabel("Principal Coordinate 2 (24.1% variance explained)")
    ax.set_title("Single-Cell Representation Manifold with 95% Confidence Ellipses")
    ax.legend(loc="upper right", frameon=False, fontsize=7.5)

    os.makedirs(output_dir, exist_ok=True)
    generated_files = {}
    for fmt in formats:
        out_path = os.path.join(output_dir, f"{CHART_ID}_{style}.{fmt}")
        fig.savefig(out_path, format=fmt, dpi=dpi, bbox_inches="tight")
        generated_files[fmt] = str(Path(out_path).resolve())

    plt.close(fig)
    return generated_files


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Render PCA / UMAP Projection with Ellipses")
    parser.add_argument("--data", type=str, default=None, help="Path to input CSV dataset")
    parser.add_argument("--style", type=str, default="nature", choices=["nature", "cell", "lancet", "ieee", "economist"], help="Journal style pack")
    parser.add_argument("--output-dir", type=str, default="output", help="Output directory")
    parser.add_argument("--formats", type=str, default="svg,pdf,png,tiff", help="Comma-separated export formats")
    parser.add_argument("--dpi", type=int, default=600, help="Raster DPI")
    args = parser.parse_args()

    fmt_list = [f.strip() for f in args.formats.split(",") if f.strip()]
    results = render(args.data, style=args.style, output_dir=args.output_dir, formats=fmt_list, dpi=args.dpi)
    for fmt, path in results.items():
        print(f"Generated {fmt.upper()}: {path}")