F_\text{st} 解析ハンズオン — 候補遺伝子・SNPの抽出
概要
研究室内ハンズオンの HTML 版。 未公開データを使ったもののため、一部結果を非表示にしてあります。
目標
他サンプルをマージした VCF ファイルからスタートして、 F_\text{st} のマンハッタンプロットとハプロタイプのヒートマップを描く。
事前準備
遺伝研スパコンのアカウントを作成し、 ログインできる状態にする。
手元の PC に R の解析環境を用意する。
NCBI から
bGalGal1.mat.broiler.GRCg7b
の Sequence Report をダウンロードする:https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_016699485.2/ へ
Chromosomes までスクロールして Download から取得
材料
- 日本鶏1品種1サンプルの VCF ファイル
-
ファイルの名前は
varsites.snponly.vcf.gz
- 変異がある座位のみをコール。INDEL を除いた SNP のみの VCF。
自分のディレクトリにインデックス (.csi
) と一緒にコピーする:
使うソフトウェア
いずれも遺伝研ではデフォルトで使える。 他のバージョンのものが良ければ apptainer
越しに使う。
vcftools --version
# VCFtools (0.1.16)
bcftools --version
# bcftools 1.13
bedtools --version
# bedtools v2.30.0
## or use via apptainer, e.g.
apptainer exec /usr/local/biotools/v/vcftools:0.1.16--h9a82719_5 vcftools --version
# VCFtools (0.1.16)
F_\text{st} を計算してマンハッタンプロットを描く
軍鶏 vs 他品種で分化しているゲノム領域を特定する。 VCFtools を使う。
遺伝研での作業なので、以降の操作はシェルスクリプト (.sh
) を書いて qsub
してもよい。 今回は便宜上、通常のコマンドラインで実行する形で説明する。
毎回 VCF を指定するのは面倒なので変数に代入して ${vcf}
で使う:
vcf=YOUR_DIR/varsites.snponly.vcf.gz
2集団のサンプル名を抽出する
bcftools query
と grep
を組み合わせて軍鶏とそれ以外のサンプル名を それぞれテキストファイルに格納する:
# 軍鶏はサンプル名に `SM` を含む。これと八木戸 `YKD` を抽出。
bcftools query -l ${vcf} | grep -E 'SM|YKD' > shamo.txt
bcftools query -l ${vcf} | grep -v -E 'SM|YKD' > non-shamo.txt
F_\text{st} を計算する
2集団と window-size
、window-step
を指定して F_\text{st} を計算する:
vcftools --gzvcf ${vcf} --weir-fst-pop shamo.txt --weir-fst-pop non-shamo.txt \
--fst-window-size 10000 --fst-window-step 5000 --out shamo_vs_other
--out
で指定した Prefix で .windowed.weir.fst
と .log
の2ファイルができる。
マンハッタンプロットを描く
ここからローカルでの作業に移る。 shamo_vs_other.windowed.weir.fst
を手元にダウンロードする。 これと事前準備3で取得した sequence_report.tsv
を使う。
draw_manhattan_plot.R
## パッケージのダウンロード
install.packages("conflicted")
install.packages("tidyverse")
install.packages("qqman")
## パッケージの読み込み
library(conflicted) # おまじない
library(tidyverse) # データの操作用
library(qqman) # マンハッタンプロット用
## sequence_report.tsv を読み込む:
= readr::read_tsv("sequence_report.tsv") |>
acc2chr::rename(CHROM = `RefSeq seq accession`, chr = `Chromosome name`) |> # 列名の変更
dplyr::select(CHROM, chr) # 染色体のaccessionと番号のみ抽出
dplyr
## shamo_vs_other.windowed.weir.fst を読み込む
= readr::read_tsv("shamo_vs_other.windowed.weir.fst") |>
fst ::rownames_to_column(var = "SNP") |>
tibble::left_join(acc2chr, by = "CHROM") |>
dplyr::mutate(chr = readr::parse_integer(chr, na = c("W", "Z", "Un", "MT"))) |>
dplyr::filter(!is.na(chr))
dplyr
## マンハッタンプロットの描画
::manhattan(
qqman
fst,chr = "chr",
bp = "BIN_START",
p = "WEIGHTED_FST", # or "MEAN_FST"
snp = "SNP",
col = c("#1f78b4", "#a6cee3"),
logp = FALSE,
xlab = "Chromosome",
ylab = "Fst"
)
qqman::manhattan()
はデフォルトでは PLINK の出力ファイルである BP 列、CHR 列、P 列、SNP 列をもつデータフレームを想定する。 これ以外のデータでマンハッタンプロットの描画をするときは、引数に列名などを指定していく。
chr
- 横軸の染色体番号
bp
- 各染色体上の SNP の座位
p
-
縦軸の統計量。 デフォルトの想定は GWAS 等の P-value なので
p
になっている。 snp
-
各 SNP の名前。典型的なのは
rs123456
とか。 col
-
染色体の色。今回は
#1f78b4
■ と#a6cee3
■ を使ってみる。 logp
-
縦軸
p
を対数変換するかどうか。 今回は F_\text{st} の生の値を使うのでFALSE
。 xlab
,ylab
- 横軸、縦軸のラベル
F_\text{st} の高い領域に含まれる遺伝子をリストする
bedtools
による重なり抽出
マンハッタンプロットをみて、いい感じに閾値を決める。 今回 F_\text{st} > 0.3 でやってみる。
ここでまた遺伝研に戻り、bedtools
を使って F_\text{st} の高い領域とオーバーラップする遺伝子をリストする。
AWK コマンドで F_\text{st} > 0.3 の行を抽出:
awk -F'\t' '$5 > 0.3' shamo_vs_other.windowed.weir.fst > shamo_vs_other.03.fst
.fst
ファイルの5列目 ($5
) が WEIGHTED_FST
なので、これが 0.3 より大きい行を抽出する。 この作業自体はエクセルでやっても R でやってもいい。
NCBI から bGalGal1.mat.broiler.GRCg7b
の GTF をダウンロードする:
- https://www.ncbi.nlm.nih.gov/datasets/genome/GCF_016699485.2/ へ
- Download から Annotation features (GTF) のみ選択してダウンロード
- 手元に取得した
genomic.gtf
を遺伝研に送る
このまま使ってもいいけど、9列目の Attribute の情報が多すぎる。 今回は遺伝子ID (gene_id
) さえあればいいので、ここだけ残したシンプルな GFF に変えておく:
clean_gtf.py
import argparse
def main():
= argparse.ArgumentParser()
parser "-i", "--infile")
parser.add_argument("-o", "--outfile")
parser.add_argument(= parser.parse_args()
args
= clean_gtf(args.infile)
gtf with open(args.outfile, "w") as f:
for l in gtf:
"\t".join(l)+"\n")
f.write(
def clean_gtf(gtf):
= []
new_lines with open(gtf) as f:
for l in f:
if l[0] == "#":
"\n")])
new_lines.append([l.rstrip(else:
= l.rstrip("\n").split("\t")
l -1] = extract_gene_from_gtf(l[-1])
l[
new_lines.append(l)return new_lines
def extract_gene_from_gtf(desc):
= desc.split("; ")
descs = {i.split(" ")[0]: i.split(" ")[1].strip('"') for i in descs if " " in i}
descs = descs.get("gene_id", "NA")
gene return gene
if __name__ == "__main__":
main()
python3 clean_gtf.py -i genomic.gtf -o simple.gtf
bedtools intersect
コマンドで、.fst
と .gtf
のオーバーラップする領域を抽出する:
bedtools intersect -a shamo_vs_other.03.fst -b simple.gtf -wo > shamo_vs_other.03.annot.fst
再びマンハッタンプロットの描画
shamo_vs_other.03.annot.fst
を再び手元にダウンロードして R で可視化:
draw_manhattan_plot.R の続き
= c("CHROM", "BIN_START", "BIN_END", "N_VARIANTS", "WEIGHTED_FST", "MEAN_FST")
cols= readr::read_tsv("./shamo_vs_other.03.annot.fst", col_names = cols) |>
fst2gene ::rename("gene_id" = X15) |>
dplyr::distinct(BIN_START, gene_id, .keep_all = TRUE) |>
dplyrgroup_by(CHROM, BIN_START) |>
summarize(gene_id = paste(gene_id, collapse = ", "))
= fst |> left_join(fst2gene, by = c("CHROM", "BIN_START"))
fst
::manhattan(
qqman
fst,chr = "chr",
bp = "BIN_START",
p = "WEIGHTED_FST", # or "MEAN_FST"
snp = "gene_id",
col = c("#1f78b4", "#a6cee3"),
logp = FALSE,
xlab = "Chromosome",
ylab = "Fst",
annotatePval = 0.3,
annotateTop = FALSE
)
annotatePval
-
縦軸がこの値以上の点の
snp
を表示する。 -
今回は F_\text{st} > 0.3 でやっているので
0.3
を指定 annotateTop
-
TRUE
だと各染色体の Top のみを表示する。 -
いったん
FALSE
で全部表示してみて、重なりすぎてて見にくければTRUE
解釈
こうして絞り込んだ遺伝子リストから、 さらにジェノタイピングやハプロタイプ解析に回す遺伝子を探していく。
今回、2番染色体のピークに座上する ISPD は、 軍鶏を対象とした先行研究 (Luo et al. 2020, Bendesly et al. 2024) でも得られており、有力な候補と言える。
この後は、ISPD の周辺領域を抽出してハプロタイプ解析を行っていく。
VCF から候補遺伝子の周辺領域を抽出
前半の F_\text{st} 解析で候補遺伝子で抽出した ISPD の周辺領域を SNP 単位で解析していく。
ISPD 領域
NCBI で ISPD のページを見ると、 2番染色体 (NC_052533.1
) の 28184591..28296806 にあるらしい。
この前後 5000bp を VCF から抽出する:
vcf=YOUR_DIR/varsites.snponly.vcf.gz
vcftools --gzvcf ${vcf} \
--chr NC_052533.1 --from-bp $((28184591-5000)) --to-bp $((28296806+5000)) \
--recode --out ISPD
--chr
で染色体 accession、--from-bp
/--to-bp
で座位を指定して VCF の特定領域を抽出する。 --out
で指定した Prefix で、ISPD.recode.vcf
が生成される。
ハプロタイプ解析
抽出した ISPD 周辺領域の VCF を手元にダウンロードして、 R で読み込む。 全領域を可視化するとプロットが横に潰れるので、F_\text{st} が特に高い領域に絞っておく:
draw_haplotype_plot.R
library(conflicted)
library(tidyverse)
= readr::read_tsv("ISPD.recode.vcf", comment = "##") |> # もしくは ISPD.snpEff.vcf
ispd_vcf ::filter(POS >= 28200001 & POS <= 28215000) |>
dplyr::pivot_longer(
tidyr::starts_with("bam/"),
dplyrnames_to = "sample",
values_to = "genotype"
|>
) ::mutate(
dplyrPOS = as.character(POS),
sample = stringr::str_split(sample, "/", simplify = TRUE)[,2],
genotype = stringr::str_replace(genotype, ":.+", ""),
FORMAT = "GT",
category = dplyr::if_else(stringr::str_detect(sample, "SM|YKD"), 1, 0),
sample = forcats::fct_reorder(sample, category)
)
\Delta Allele frequency の計算と可視化
軍鶏とその他品種のアリル頻度の差を計算する。 ここは VCF の形式によって方法が異なる。 今回は bcftools mpileup | bcftools call --ploidy 2
でバリアントコールした形式を想定している。
この場合、あるアリルの遺伝子型が2本とも Reference と同じ (REF/REF
) であれば 0/0
、 ヘテロで SNP を持つ場合 (REF/ALT
) 0/1
、ホモで持つ場合 (ALT/ALT
) 1/1
となる。
ALT
遺伝子型のアリル頻度を、0/0
= 0, 0/1
= 0.5, 1/1
= 1 としてその平均をとって計算する:
draw_haplotype_plot.R の続き
= ispd_vcf |>
ispd_af ::select(POS, sample, genotype, category) |>
dplyr::mutate(
dplyrAF = dplyr::case_when(
== "1/1" ~ 0,
genotype == "0/1" ~ 0.5,
genotype .default = 1),
category = dplyr::if_else(category == 1, "Shamo_AF", "Other_AF")
|>
) ::group_by(POS, category) |>
dplyr::summarise(mean_AF = mean(AF)) |>
dplyr::pivot_wider(names_from = category, values_from = mean_AF) |>
tidyr::mutate(diff_AF = abs(Shamo_AF - Other_AF)) dplyr
これを ggplot::geom_col()
で可視化する:
draw_haplotype_plot.R の続き
::ggplot(ispd_af) +
ggplot2aes(x = POS, y = diff_AF) + # 横軸に座位、縦軸にアリル頻度の差
geom_col(fill = "#999999") + # バープロットの描画
labs(x = "POS", y = "delta allele frequency") + # x軸, y軸のタイトル
theme_classic() + # シンプルなテーマで
theme(
axis.text.x = element_blank(), # x軸のラベルはなくす
axis.ticks.x = element_blank() # x軸の ticks もなくす
)
ハプロタイプのヒートマップ描画
続いて、各品種の遺伝子型をヒートマップ形式で可視化する。
draw_haplotype_plot.R の続き
::ggplot(ispd_vcf) +
ggplot2aes(x = POS, y = sample) + # 横軸に座位、縦軸にサンプル
geom_tile(aes(fill = genotype)) + # タイルプロット。色分けはgenotypeで。
scale_fill_viridis_d(option = "cividis") + # 色分けの色の指定
labs(x = "POS", y = "", fill = "Genotype") + # x軸, y軸, 色分けのタイトル
theme_bw() +
theme(
axis.text.x = element_blank(), # x軸のラベルはなくす
axis.ticks.x = element_blank(), # x軸のticksもなくす
)
このようにしてアリル頻度の集団間差が特に大きい SNP を絞り込んでいく。 snpEff などを組み合わせれば、 その中でアミノ酸変異を伴うもの、といった絞り込みも可能。
PBS の計算
原理
2集団の F_\text{st} では、得られるピークがどちらの集団のアリル頻度変化によるものかが区別できない。 そこで3集団目を追加し、F_\text{st} を枝長とみなすことで特定の集団でアリル頻度が変化した領域を特定する手法が PBS (Population Branch Statistics) である。 (Yi et al. 2010)
3集団 (A, B, C) のペアワイズで F_\text{st} を算出して、各2集団間の枝長を求める:
T = -\log{(1-F_\text{st})}
各集団間の枝長を T_\text{AB}, T_\text{BC}, T_\text{CA}, として、 例えば集団 A の PBS を求めたければ、次のようにする:
PBS_\text{A} = \frac{T_\text{AB} + T_\text{CA} - T_\text{BC} }{2}
R で実装
PBS を算出するソフトウェアはいくつかあるが (例えば scikit-allel や kpbs)、 ここでは VCFtools の出力を使って R で PBS を計算する。
まず、3集団の総当たりで算出した F_\text{st} を読み込む。 ここでは仮に pop1、pop2、pop3 とする:
## パッケージの読み込み
library(conflicted) # おまじない
library(tidyverse) # データの操作用
## sequence_report.tsv を読み込む:
= readr::read_tsv("sequence_report.tsv") |>
acc2chr::rename(CHROM = `RefSeq seq accession`, chr = `Chromosome name`) |> # 列名の変更
dplyr::select(CHROM, chr) # 染色体のaccessionと番号のみ抽出
dplyr
## 3集団の総当たり Fst を読み込む
= readr::read_tsv("pop1_vs_pop2.windowed.weir.fst") |>
fst_pop1_pop2 ::rownames_to_column(var = "SNP") |>
tibble::left_join(acc2chr, by = "CHROM") |>
dplyr::mutate(chr = readr::parse_integer(chr, na = c("W", "Z", "Un", "MT"))) |>
dplyr::filter(!is.na(chr))
dplyr
= readr::read_tsv("pop2_vs_pop3.windowed.weir.fst") |>
fst_pop2_pop3 ::rownames_to_column(var = "SNP") |>
tibble::left_join(acc2chr, by = "CHROM") |>
dplyr::mutate(chr = readr::parse_integer(chr, na = c("W", "Z", "Un", "MT"))) |>
dplyr::filter(!is.na(chr))
dplyr
= readr::read_tsv("pop3_vs_pop1.windowed.weir.fst") |>
fst_pop3_pop1 ::rownames_to_column(var = "SNP") |>
tibble::left_join(acc2chr, by = "CHROM") |>
dplyr::mutate(chr = readr::parse_integer(chr, na = c("W", "Z", "Un", "MT"))) |>
dplyr::filter(!is.na(chr)) dplyr
次に F_\text{st} を枝長に変換する:
= fst_pop1_pop2 |>
t_pop1_pop2 ::mutate(t_pop1_pop2 = -log(1 - WEIGHTED_FST)) |>
dplyr::select(CHROM, chr, BIN_START, BIN_END, t_pop1_pop2)
dplyr
= fst_pop2_pop3 |>
t_pop2_pop3 ::mutate(t_pop2_pop3 = -log(1 - WEIGHTED_FST)) |>
dplyr::select(CHROM, chr, BIN_START, BIN_END, t_pop2_pop3)
dplyr
= fst_pop3_pop1 |>
t_pop3_pop1 ::mutate(t_pop3_pop1 = -log(1 - WEIGHTED_FST)) |>
dplyr::select(CHROM, chr, BIN_START, BIN_END, t_pop3_pop1) dplyr
最後にデータフレームを結合し、PBS を算出する。 ここでは例として pop1 における PBS を計算する。
## 対象の集団が含まれる T 2つを足したものから対象の集団が含まれない T を引いて2で割る
= t_pop1_pop2 |>
df_pbs ::inner_join(t_pop2_pop3, by = c("CHROM", "chr", "BIN_START", "BIN_END")) |>
dplyr::inner_join(t_pop3_pop1, by = c("CHROM", "chr", "BIN_START", "BIN_END")) |>
dplyr::mutate(pbs_pop1 = (t_pop1_pop2 + t_pop3_pop1 - t_pop2_pop3)/2) dplyr