この記事のゴール
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枚の表に詰め込むのは無理があります。生カウントと正規化カウントは「遺伝子ごと」の表ですが、擬似時間やクラスタ番号は「1細胞に1つの値」、座標はまた別の形…と、データの形がバラバラだからです。
ここで Excel を思い浮かべてください。自然なのは、用途ごとにシートを分けることです。
- シート1:生カウント(行=細胞、列=遺伝子)
- シート2:正規化カウント(行=細胞、列=遺伝子)
- シート3:細胞の属性(行=細胞、列=擬似時間・クラスタ番号・UMAP座標…)
ポイントは、どのシートも「行=同じ細胞ID」でそろえておくことです。こうすれば、後からいくらデータを足しても、同じ細胞の情報を常に突き合わせられます。
💡 なぜ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 の2つの利点
AnnData がこれだけ使われるのは、上の構造のおかげで次の2つの利点があるからです。
① 細胞群をまるごと抽出できる
obs の列を条件にすれば、欲しい細胞だけを一発で取り出せます。しかも X だけでなく、obs・obsm・layers など対応するデータがまとめて付いてきます(同じ細胞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 # 新しい次元削減の座標
💡 なぜ簡単にできるのか
この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を退避しておくのが安全。
関連記事
- 📖 scRNA-seq前処理の全体像 / 正規化・特徴選択・次元削減・クラスタリング:AnnData を更新していく流れの全体像
- 📖 scVIだけじゃない:scvi-tools の全体像:AnnData を入力にする深層生成モデル群
- 📖 ダブレットの検出(Scrublet/SOLO):生カウントと layers の使い分けが効く工程
- 📖 アノテーション編 細胞型アノテーション手法の全体像:
obsに細胞型ラベルを書き戻す各手法
参考文献
- 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


コメント