scRNA-seq解析の土台:AnnDataのデータ構造をリレーショナルDBの発想で理解する

scRNA-seq

📚 この記事について:「ツール解説」シリーズ。Python の scRNA-seq 解析(Scanpy・scvi-tools など)の中心データ構造 AnnData を、まず身近な例(増え続けるデータをどう管理するか)から直感的に説明し、最後にリレーショナルデータベース(RDB)の発想として整理します。これが分かると、前処理・アノテーション・統合のすべての記事が読みやすくなります。

🧩 前提:Python の基本。表(テーブル)と行・列の概念。


この記事のゴール

AnnData(Annotated Data の略)の構造を理解し、.X / .obs / .var / .layers / .raw / .obsm / .unsそれぞれ何を保持し、なぜこの設計なのかを、直感的に説明できること。


まず、解析は「行列」から始まる

シングルセルRNA-seq 解析は、「細胞 × 遺伝子」のただの行列から始まります。各細胞で各遺伝子が何回検出されたか(生カウント)を並べた表です。スタート地点では、データはこれだけです。


解析が進むと、1つの細胞にデータが積み上がる

ところが解析が進むにつれて、付随するデータがどんどん増えていきます。1個の細胞に着目して追ってみましょう。

  • 最初:その細胞の生カウント(各遺伝子の検出回数)だけ。
  • 正規化すると:生カウントに加えて正規化済みカウントが増える。これで同じ細胞に「生」「正規化」の2種類のカウントが並ぶ。
  • 次元削減するとPCA / UMAP 上の座標が記録される。
  • 擬似時間解析をすると:幹細胞を 0、分化の終着点を 1 としたときの相対的な時間(例:0.3)が付く。
  • クラスタリングすると:その細胞が何番のクラスタかが付く。さらに再クラスタリングすると、1回目のクラスタ番号2回目のクラスタ番号を別々に持つ必要が出てくる。

つまり、シングルセル解析の計算結果は、1細胞あたりに解析が進むほど際限なく積み上がっていきます

1つの細胞に着目すると、解析が進むほどデータの種類が増えていく。生カウントだけだった状態から、正規化カウント・PCA/UMAP座標・擬似時間・1回目/2回目のクラスタ番号と追加されていく。しかも、それぞれ形(遺伝子ごと/1細胞に1値/座標)が違う。
1つの細胞に着目すると、解析が進むほどデータの”種類”が増えていく。生カウントだけだった状態から、正規化カウント・PCA/UMAP座標・擬似時間・1回目/2回目のクラスタ番号…と追加されていく。しかも、それぞれ形(遺伝子ごと/1細胞に1値/座標)が違う。

この増え続けるデータをどう管理する?

では、この増え続けるデータをどう管理すればよいでしょうか。

全部を1枚の表に詰め込むのは無理があります。生カウントと正規化カウントは「遺伝子ごと」の表ですが、擬似時間やクラスタ番号は「1細胞に1つの値」、座標はまた別の形…と、データの形がバラバラだからです。

ここで Excel を思い浮かべてください。自然なのは、用途ごとにシートを分けることです。

  • シート1:生カウント(行=細胞、列=遺伝子)
  • シート2:正規化カウント(行=細胞、列=遺伝子)
  • シート3:細胞の属性(行=細胞、列=擬似時間・クラスタ番号・UMAP座標…)

ポイントは、どのシートも「行=同じ細胞ID」でそろえておくことです。こうすれば、後からいくらデータを足しても、同じ細胞の情報を常に突き合わせられます

増え続けるデータは、用途ごとのシートに分け、すべて「同じ細胞ID(キー)」でそろえると管理できる。生/正規化カウントは X/layers、細胞の属性(擬似時間・クラスタ番号)は obs、座標は obsm、遺伝子の情報は var に対応する。AnnData はこれを1つのオブジェクトに実装したもの。
増え続けるデータは、用途ごとのシートに分け、すべて「同じ細胞ID(キー)」でそろえると管理できる。生/正規化カウントは X/layers、細胞の属性(擬似時間・クラスタ番号)は obs、座標は obsm、遺伝子の情報は var に対応する。AnnData はこれを1つのオブジェクトに実装したもの。

💡 なぜ1枚の表ではダメか
「遺伝子ごとの値(カウント)」「1細胞に1つの値(擬似時間・クラスタ番号)」「座標(多次元)」は、形が違うので同じ表には収まりません。だから用途ごとに表を分け、共通のキー(細胞ID)で結ぶのが合理的です。


これがリレーショナルDBの発想、そして AnnData

複数の表を、共通のID(キー)で結びつけて管理する」――これはまさにリレーショナルデータベース(RDB)の発想です。そして AnnData は、この発想を1つのオブジェクトに実装したものです。

  • 遺伝子ごとの表(生・正規化カウント)→ .X.layers
  • 細胞の属性(擬似時間・クラスタ番号など)→ .obs
  • 細胞ごとの座標(UMAP・PCA)→ .obsm
  • 遺伝子の情報 → .var

そして、すべてが同じ細胞ID(obs_names)と遺伝子ID(var_names)で整合しています。obs の i 番目は X の i 行目、var の j 番目は X の j 列目――と、インデックス(=キー)で常に対応します。だから、行や列を絞っても対応するデータが自動でそろい、崩れません

AnnData の構造。中心の発現行列 X に、行(細胞)のテーブル obs と列(遺伝子)のテーブル var が、インデックス(=キー)で結合している。さらに layers・raw・obsm・obsp・uns といった付随スロットが、同じキーで X に紐づく。
AnnData の構造。中心の発現行列 X に、行(細胞)のテーブル obs と列(遺伝子)のテーブル var が、インデックス(=キー)で結合している。さらに layers・raw・obsm・obsp・uns といった付随スロットが、同じキーで X に紐づく。

AnnData の2つの利点

AnnData がこれだけ使われるのは、上の構造のおかげで次の2つの利点があるからです。

① 細胞群をまるごと抽出できる

obs の列を条件にすれば、欲しい細胞だけを一発で取り出せます。しかも X だけでなく、obsobsmlayers など対応するデータがまとめて付いてきます(同じ細胞ID で結合しているため)。

python# ニューロンだけを抽出(X・obs・obsm・layers もそろって付いてくる)
neurons = adata[adata.obs["cell_type"] == "Neuron"].copy()

# 複数条件・クラスタ指定もそのまま書ける
homo_excit = adata[(adata.obs["genotype"] == "Homo") &
                   (adata.obs["leiden"].isin(["3", "5"]))].copy()

スライスは元データのビュー(view)を返すことがあるため、独立に扱うなら .copy() で実体化します。

② 追加解析の結果を後から足せる

新しい解析で出た結果(クラスタ番号・擬似時間・新しい埋め込み)は、同じ adata書き足すだけ。同じキー(細胞ID)で紐づくので、既存のデータとズレません。

python# 細胞ごとの値は obs に足す
adata.obs["pseudotime"] = pseudotime_values   # 擬似時間
adata.obs["leiden_2"]   = relabeled           # 2回目のクラスタ番号

# 多次元の結果(埋め込み)は obsm に足す
adata.obsm["X_newmethod"] = embedding         # 新しい次元削減の座標
AnnData の2つの利点。① obs の条件で細胞群を抽出すると、X・obs・obsm・layers がまとめて付いてくる。② 追加解析で出た結果(擬似時間など)は obs / obsm に列として足すだけで、同じ細胞ID(キー)で結合するため既存データとズレない。
AnnData の2つの利点。① obs の条件で細胞群を抽出すると、X・obs・obsm・layers がまとめて付いてくる。② 追加解析で出た結果(擬似時間など)は obs / obsm に列として足すだけで、同じ細胞ID(キー)で結合するため既存データとズレない。

💡 なぜ簡単にできるのか
この2つができるのは、すべてが「細胞ID・遺伝子ID(キー)」で結合しているから。抽出しても結合が保たれ、追記しても他と整合します。


各スロットの役割

スロット 役割 さきほどの例で言うと
.X n_obs × n_vars メインの発現行列 「現在使う」カウントの表
.layers n_obs × n_vars(複数) X と同じ形の別バージョン 生カウント/正規化カウントの各シート
.obs DataFrame(n_obs 行) 細胞のメタデータ(total_counts, leiden, cell_type, 擬似時間 …) 細胞の属性シート(キー=obs_names)
.var DataFrame(n_vars 行) 遺伝子のメタデータ(highly_variable, mean …) 遺伝子の属性表(キー=var_names)
.obsm / .varm n_obs × d / n_vars × d 行 / 列に紐づく多次元配列(X_pca, X_umap, X_scVI / PCs) 各細胞の座標(UMAP・PCA)
.obsp / .varp n_obs × n_obs / n_vars × n_vars 細胞間 / 遺伝子間のペアワイズ(neighbors) 細胞どうしの近さ
.raw 別の AnnData サブセット前の状態(HVG で絞る前の全遺伝子など) バックアップのシート
.uns 自由な辞書 表に乗らない補足(パラメータ・色・PCA 分散比) メモ欄

実際に触る

pythonimport scanpy as sc

# 読み込み(h5ad ファイル)
adata = sc.read_h5ad("data.h5ad")

# まず全体像を見る
print(adata)                 # 次元・各スロットの一覧が表示される
print(adata.shape)           # (n_obs, n_vars) = (細胞数, 遺伝子数)
adata.obs.head()             # 細胞のテーブル(行のメタデータ)
adata.var.head()             # 遺伝子のテーブル(列のメタデータ)
python# 生カウントを layers に退避(正規化の前に)
adata.layers["counts"] = adata.X.copy()

# HVG で列を絞る前に、全遺伝子を raw に退避しておく
adata.raw = adata

# 保存(構造ごと1ファイルに)
adata.write("processed.h5ad")

🔧 環境メモprint(adata) を実行すると、X の形と obs/var/layers/obsm/uns などの一覧が確認できます。まず構造を眺めるのが理解の近道です。


落とし穴

⚠️ つまずきやすい点
.X の中身は「生カウント」か「正規化済み」か、オブジェクト自体は区別しません。どちらが入っているかは自分で管理し、layers(例:counts / lognorm)で使い分けます。ツールによって要求が違います(例:scVI は生カウント、CellTypist は log 正規化)。
HVG で列を絞る前に adata.raw = adata をしておく。退避しないと、全遺伝子が必要なツール(CellTypist など)で困ります。
obs_names / var_names は一意に。重複があると結合(キー)が壊れます。
ビューと実体:絞り込んだ後に書き換えるなら .copy()


まとめ

  • シングルセル解析は「細胞 × 遺伝子」の行列から始まり、解析が進むほど1細胞あたりのデータが積み上がる(生→正規化→座標→擬似時間→クラスタ番号…)。
  • 形の違うデータは、用途ごとに表を分け、同じ細胞ID(キー)でそろえると管理できる。これがリレーショナルDBの発想であり、AnnData はそれを1つのオブジェクトに実装したもの。
  • .X/.layers(カウント)・.obs(細胞の属性)・.obsm(座標)・.var(遺伝子情報)が、すべて同じキーで整合。だから後から足しても崩れない。
  • 実務では .X の中身を自分で管理し、HVG で絞る前に raw を退避しておくのが安全。

関連記事


参考文献

  • 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
  • Virshup, I., Rybakov, S., Theis, F. J., Angerer, P., & Wolf, F. A. (2024). anndata: Annotated data. Journal of Open Source Software, 9(101), 4371. doi:10.21105/joss.04371
  • Virshup, I., Bredikhin, D., Heumos, L., et al. (2023). The scverse project provides a computational ecosystem for single-cell omics data analysis. Nature Biotechnology, 41, 604–606. doi:10.1038/s41587-023-01733-8

コメント

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