シーケンス型 空間トランスクリプトーム:QC と組織検出 ─ 大域的な閾値が組織ドメインを消す

Spatial transcriptome
📚 この記事について
組織検出の確認から、QC 指標の計算、閾値の決め方までを扱います。scRNA-seq の QC をそのまま持ち込むと何が起きるのか、なぜそうなるのかを具体的に見ていきます。
🔙 前の記事:シーケンス型 空間トランスクリプトーム:データ読み込みとデータ構造
🔜 次の記事:シーケンス型 空間トランスクリプトーム:解析単位の決め方
📌 前提scRNA-seq解析:データの読み込みと品質管理(QC)AnnData のデータ構造

空間トランスクリプトーム解析(spatial transcriptomics)のうちシーケンス型(Visium・Stereo-seq など NGS で読み出す方式)は、QC で使う指標そのものは scRNA-seq と変わりません。総カウント、検出遺伝子数、ミトコンドリア率。関数もそのまま動きます。ところが、同じ指標に同じ閾値を当てはめると、組織のある領域がまるごと消えます。エラーは出ません。ただ、白質や間質や脂肪組織が、静かにデータから抜け落ちる。この記事では、なぜそうなるのか、どう避けるのかを説明します。

1. その前に ─ 組織検出は本当に成功しているか


QC の話をする前に、確認しておくことがあります。組織の外にあるフィーチャが、まだデータに残っていないか

Space Ranger や SAW は、画像から組織の範囲を自動で判定します。しかしこのアルゴリズムには前提があります。10x Genomics 自身が書いているとおり、「明るく均一な背景の上に、暗くて構造の複雑な組織」があることを期待しています。この前提が崩れる場面は、実際にはよくあります。

図:自動の組織検出は、何を前提にしているか
図:自動の組織検出は、何を前提にしているか
⚠️ 失敗しても、パイプラインは失敗を教えてくれない
10x Genomics のドキュメントには、「多くの場合、パイプラインは失敗が起きたことを示すことができない」と明記されています。つまり、組織検出が半分ずれていても、Space Ranger は正常終了します。出力される web_summary.html の組織検出画像を、毎回自分の目で確かめるしかありません。組織の範囲が赤や青で描かれるので、実際の組織と重なっているかを見ます。

ずれていた場合は、Loupe Browser の手動整列ツールで組織範囲を選び直し、JSON を書き出して spaceranger count に –loupe-alignment で渡します。

⚠️ 手動整列でよくある失敗
Loupe Browser に渡す画像を「転送が楽だから」と縮小してしまうと、書き出した JSON のピクセル座標が、Space Ranger に渡すフル解像度画像と合いません。この JSON には画像のチェックサムが入っており、Space Ranger は自分が受け取った画像から計算したチェックサムと照合します。一致しなければパイプラインは停止します。Loupe と Space Ranger には、まったく同じ画像を渡してください。

組織検出が甘いと、QC にそのまま波及します。組織の外のフィーチャはカウントがほぼゼロなので、総カウントの分布の左側が「ほぼ空のフィーチャ」で埋まり、閾値をどこに置くべきかが判断できなくなります。

2. QC 指標を計算する


計算そのものは scRNA-seq の QCと同じです。総カウント、検出遺伝子数、ミトコンドリア率。空間データでは、これにヘモグロビン率を足しておくと役に立ちます。

python
import scanpy as sc
import squidpy as sq

# 解析単位を決めてから QC する(ここでは 8 µm ビン)
adata = sdata["square_008um"].copy()

# ミトコンドリア遺伝子
adata.var["mt"] = adata.var_names.str.startswith("MT-")
# ヘモグロビン遺伝子(HBP 擬似遺伝子は除く)
adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]", regex=True)

sc.pp.calculate_qc_metrics(
    adata, qc_vars=["mt", "hb"], inplace=True, log1p=True,
)
print(adata.obs[["total_counts", "n_genes_by_counts", "pct_counts_mt"]].describe())
指標 scRNA-seq での意味 シーケンス型での意味
total_counts 細胞あたりの RNA 量。低ければ細胞が壊れている フィーチャあたりの RNA 量。細胞密度と組織の性質も反映する
n_genes_by_counts 検出感度。低ければ品質が悪い 同上。ただし低カウント領域では当然低くなる
pct_counts_mt 高ければ細胞死・ストレス 高いのが正常な組織がある(心筋・肝・褐色脂肪)
pct_counts_hb 血液のコンタミ 血管の位置を示す。生物学的に正しい信号
💡 ヘモグロビンは邪魔者ではない
scRNA-seq では、ヘモグロビン遺伝子の高発現は赤血球の混入を意味し、除外の対象になります。空間データでは違います。血管の走行にきれいに沿って高くなるので、組織の構造が正しく捉えられているかの確認に使えます。血管とまったく無関係な場所で高いなら、そのときこそコンタミを疑います。

3. 指標は、ヒストグラムではなく組織の上で見る


scRNA-seq の QC では、まずヒストグラムやバイオリンプロットを描きます。空間データでは、それだけでは判断できません。同じヒストグラムが、まったく違う2つの状況から生じるからです。

  • 組織の一部の領域が、もともと RNA の少ない組織(白質・脂肪・線維化・壊死)である → 正しいデータ
  • 切片の端で透過処理がうまくいかず、カウントが落ちている → 技術由来

この2つはヒストグラム上では区別がつきません。組織の上に描いて初めて分かります。

python
# QC は、ヒストグラムではなく組織の上で見る
sq.pl.spatial_scatter(
    adata,
    color=["total_counts", "n_genes_by_counts", "pct_counts_mt", "pct_counts_hb"],
    shape=None, size=1, ncols=2,
)
# 見るべき点
#   低カウントの領域は、組織像のどこに当たるか
#   境界は直線的か(技術由来)、それとも組織の形に沿うか(生物学的)
#   ヘモグロビンが高い場所は血管か(生物学的に正しい)

4. 大域的な閾値が、組織ドメインをまるごと消す


ここがこの記事の核心です。

scRNA-seq の標準的な QC では、固定閾値(総カウント 500 未満を除く、など)か、MAD(中央値絶対偏差)による自動閾値を使います。「全フィーチャの中央値から 3 MAD 以上離れているものを外れ値とする」という方法です。

これらはどちらも、すべての観測が同じ母集団から来ていることを暗黙に仮定しています。scRNA-seq なら、それでよかった。細胞はばらばらに解離されていて、どの細胞も同じ条件で捕捉されているからです。

空間データでは、この仮定が成り立ちません。白質と灰白質、腫瘍と間質は、もともとカウントが違います。同じ母集団ではないのです。

図:同じ 3 MAD でも、何と比べるかで結果が変わる
図:同じ 3 MAD でも、何と比べるかで結果が変わる

SpotSweeper の論文(Totty, Hicks & Guo, Nature Methods, 2025)は、この問題を正面から扱っています。著者らは、単一核 RNA-seq 由来の QC 手法をそのまま持ち込むと「空間的な生物学によって交絡され、空間データには不適切である」と述べています。

実際、ヒト乳がんの Visium 検体に 3 MAD の大域的な閾値を当てると、系統的にカウントが低い、大きく連続した領域がまるごと外れ値として拾われました。その領域は局所的には均一で、生物学的に意味がある可能性が高い場所です。壊れているのではなく、もともとそういう組織だっただけなのに。

⚠️ 正規化のときと、まったく同じ構造の問題
この記事の直前で扱った正規化でも、同じことが起きました。ライブラリサイズを大域的に補正すると、組織ドメインの情報が消える(Bhuva et al., Genome Biology, 2024)。QC でも、大域的な統計を当てはめると組織ドメインが消える。空間データでは、離れた場所どうしを同じ物差しで比べてはいけない。これがシーケンス型の解析を貫く原則です。
python
import numpy as np
from scipy.stats import median_abs_deviation

# scRNA-seq でよく使う、大域的な MAD による外れ値検出
def is_outlier(adata, metric, nmads=3):
    x = adata.obs[metric]
    lo = np.median(x) - nmads * median_abs_deviation(x)
    hi = np.median(x) + nmads * median_abs_deviation(x)
    return (x < lo) | (x > hi)

adata.obs["global_outlier"] = is_outlier(adata, "log1p_total_counts")

# 空間の上に描いてみる。ここで違和感に気づけるかが分かれ目
sq.pl.spatial_scatter(adata, color="global_outlier", shape=None)
# 白質や間質が、まるごと外れ値になっていないか

5. 比べる相手を「空間の近傍」に変える


解決策は驚くほど単純です。全体の中央値ではなく、空間的に近いフィーチャの中央値と比べる。

各フィーチャについて、空間的な k 近傍を取り、その近傍の中央値と MAD で頑健な z スコアを計算します。カウントや遺伝子数が近傍より 3 だけ低ければ外れ値、ミトコンドリア率が近傍より 3 だけ高ければ外れ値。白質の中にいるフィーチャは白質の仲間と比べられるので、「白質だから低い」という理由では落ちません。

python
# 空間の近傍と比べる。これが SpotSweeper の考え方
sq.gr.spatial_neighbors(adata, coord_type="generic", n_neighs=36)
W = adata.obsp["spatial_connectivities"]

def local_z(adata, key, W):
    x = adata.obs[key].to_numpy(dtype=float)
    z = np.zeros_like(x)
    for i in range(len(x)):
        nb = x[W.indices[W.indptr[i]:W.indptr[i + 1]]]
        med = np.median(nb)
        mad = np.median(np.abs(nb - med))
        # 1.4826 は、正規分布のもとで MAD を標準偏差に合わせる定数
        z[i] = 0.0 if mad == 0 else (x[i] - med) / (1.4826 * mad)
    return z

adata.obs["z_counts"] = local_z(adata, "log1p_total_counts", W)
adata.obs["z_mt"]     = local_z(adata, "pct_counts_mt", W)

# 近傍と比べて外れているものだけを落とす
bad = (adata.obs["z_counts"] < -3) | (adata.obs["z_mt"] > 3)
print("除外:", int(bad.sum()), "/", adata.n_obs)
adata = adata[~bad].copy()

この方法に切り替えると、除外されるフィーチャは組織ドメイン全体に均等に散らばります。SpotSweeper の論文では、各領域から除外されたスポットは平均 0.21〜0.40% にとどまり、代わりに微小な裂け目のような、本当に壊れた場所が拾われました。

💡 Python で使うには
SpotSweeper は R/Bioconductor パッケージが本家ですが、AnnData と直接つながる Python 実装(spotsweeper-py)も公開されています。上のコードは、その考え方を Squidpy の空間近傍グラフの上で最小限に実装したものです。仕組みが分かっていれば、10 行で書けます。

6. 領域として壊れていることもある


個々のフィーチャではなく、領域そのものがアーティファクトであることがあります。切片の端がめくれて重なった部分、剥離した部分、気泡が入った部分。こうした領域では、ミトコンドリア率が局所的に不安定になります。

SpotSweeper は、近傍のサイズを変えながらミトコンドリア率の局所分散を計算し、複数のスケールで分散が高い場所を領域アーティファクトとして検出します。1つのフィーチャだけを見ていては分からない、「まとまった範囲がおかしい」という異常を捉える発想です。

7. 技術由来か、生物学的か ─ 見分け方


低カウントの領域を見つけたときに、何を確かめればよいのか。判断の順番を整理します。

図:低カウントの領域を見つけたら、何を確かめるか
図:低カウントの領域を見つけたら、何を確かめるか
見えているもの 疑うもの 確認する方法
切片の端に沿ってカウントが落ちる 透過処理のムラ、乾燥 境界が組織の形ではなくスライドの軸に沿うか
直線的な帯・格子状のパターン 流路や位置マーカーに由来する技術ノイズ パターンが機械の座標軸と一致するか
小さく不規則な低カウント領域 裂け目・気泡・剥離 組織像に物理的な破損が見えるか
大きく連続した低カウント領域 白質・脂肪・線維化・壊死 組織像の構造と輪郭が一致するか
ミトコンドリア率が局所的に不安定 切片の折れ込み・剥離 近傍サイズを変えて局所分散を見る
ヘモグロビンが高い 血管(生物学的に正しい) 組織像で血管の走行と一致するか

8. 解析単位ごとに QC する


QC は最終的に使う解析単位の上で行います。細かいビンのままで QC をすると、これから細胞に組み立てるはずのビンを捨ててしまいます。

プラットフォーム QC を行う単位 追加で見る指標
Visium v1 / v2 55 µm スポット
Visium HD 8 µm ビン(2 µm ではやらない) 細胞に再構成した後は、細胞の面積
Curio Seeker 10 µm ビーズ ビーズあたりのカウントは極端に少ない
Stereo-seq bin50 など。cellbin なら細胞 cellbin では核あたりのカウント

9. 遺伝子側と、複数スライス


遺伝子側のフィルタは素直です。どのフィーチャでも検出されない遺伝子は落とします。

python
# 遺伝子側は迷う必要がない
sc.pp.filter_genes(adata, min_cells=10)

# 複数スライスがあるなら、分布を並べて比べる
sc.pl.violin(
    adata_all, ["total_counts", "n_genes_by_counts", "pct_counts_mt"],
    groupby="sample_id", rotation=45,
)
# 中央値が大きく違うなら、バッチ効果を疑う。QC で消さず、統合の段階で扱う
💡 プローブ捕捉のデータでは、すでに一部の遺伝子が除かれている
Visium の FFPE 系(CytAssist、Visium HD)では、オフターゲットの可能性があるプローブが Space Ranger の既定で除外されます(–filter-probes が既定で有効)。そのため、出力される遺伝子数はプローブセットの総数より少なくなります。「あるはずの遺伝子がない」ときは、まずこれを疑ってください。

複数のスライスがある場合、QC の分布をスライスごとに並べて比べます。中央値が大きく違うなら、それはバッチ効果の予兆です。ただしQC の段階で消そうとしないでください統合とバッチ補正の段階で扱うべき問題です。

10. QC チェックリスト


  • web_summary.html の組織検出画像を、自分の目で確認したか
  • 組織の外のフィーチャが残っていないか
  • 解析単位を決めてから QC したか
  • 指標を組織の上に描いて見たか(ヒストグラムだけで判断していないか)
  • 低カウント領域が、組織像のどの構造に当たるかを確認したか
  • 大域的な閾値で、組織ドメインをまるごと消していないか
  • ミトコンドリア率の閾値を、組織の種類を踏まえて決めたか
  • 複数スライスの分布の差を、QC で消そうとしていないか

まとめ


  • 自動の組織検出は「明るい背景・暗い組織」を前提にしている。失敗してもパイプラインは教えてくれないので、web_summary.html を毎回見る。
  • QC 指標は scRNA-seq と同じでよい。ヘモグロビン率を足すと血管の確認に使える。
  • 指標はヒストグラムではなく組織の上で見る。同じヒストグラムが、正しいデータと技術ノイズの両方から生じる。
  • 大域的な閾値(固定でも MAD でも)は、組織ドメインをまるごと消す。「すべてが同じ母集団」という仮定が、空間データでは成り立たないから(Totty et al., 2025)。
  • 解決は比べる相手を空間の近傍に変えること。近傍の中央値と MAD で頑健な z スコアを取る。
  • これは正規化で見た問題とまったく同じ構造。空間データでは、離れた場所どうしを同じ物差しで比べない
  • QC は最終的に使う解析単位の上で行う。

関連記事


参考文献


  • Totty, M., Hicks, S. C., & Guo, B. (2025). SpotSweeper: spatially-aware quality control for spatial transcriptomics. Nature Methods, 22(7), 1520–1530. doi:10.1038/s41592-025-02713-3
  • Bhuva, D. D., Tan, C. W., Salim, A., et al. (2024). Library size confounds biology in spatial transcriptomics data. Genome Biology, 25, 99. doi:10.1186/s13059-024-03241-7
  • Marconato, L., Palla, G., Yamauchi, K. A., et al. (2025). SpatialData: an open and universal data framework for spatial omics. Nature Methods, 22(1), 58–62. doi:10.1038/s41592-024-02212-x
  • Palla, G., Spitzer, H., Klein, M., et al. (2022). Squidpy: a scalable framework for spatial omics analysis. Nature Methods, 19(2), 171–178. doi:10.1038/s41592-021-01358-2
  • Wolf, F. A., Angerer, P., & Theis, F. J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology, 19, 15. doi:10.1186/s13059-017-1382-0
  • Ståhl, P. L., Salmén, F., Vickovic, S., et al. (2016). Visualization and analysis of gene expression in tissue sections by spatial transcriptomics. Science, 353(6294), 78–82. doi:10.1126/science.aaf2403
  • Chen, A., Liao, S., Cheng, M., et al. (2022). Spatiotemporal transcriptomic atlas of mouse organogenesis using DNA nanoball-patterned arrays. Cell, 185(10), 1777–1792. doi:10.1016/j.cell.2022.04.003

コメント

タイトルとURLをコピーしました