Selection pressure between two genomes (dN/dS)
TL;DR: dN/dS is a per-ortholog-pair measurement, so it is a colour on a
synteny track. This builds a human against rhesus macaque ortholog table with
jcvi and measures dN and dS on every pair with kaks_from_pairs.py.
Prerequisites
- jcvi
- DIAMOND
- python3 with biopython
- htslib (
bgzip,tabix) wgetnode, for the JBrowse CLI
jcvi builds C extensions and will not install against every python. If
pip install jcvi fails compiling them, uv venv --python 3.12 followed by
uv pip install jcvi biopython gets an interpreter it does build on.
Where the data comes from
The two assemblies are GCA_000001405.29 (human GRCh38) and GCA_003339765.3 (rhesus macaque Mmul_10), with gene models and coding sequence from Ensembl release 116.
- human gene models: https://ftp.ensembl.org/pub/release-116/gff3/homo_sapiens/Homo_sapiens.GRCh38.116.gff3.gz
- human coding sequence: https://ftp.ensembl.org/pub/release-116/fasta/homo_sapiens/cds/Homo_sapiens.GRCh38.cds.all.fa.gz
- rhesus macaque gene models: https://ftp.ensembl.org/pub/release-116/gff3/macaca_mulatta/Macaca_mulatta.Mmul_10.116.gff3.gz
- rhesus macaque coding sequence: https://ftp.ensembl.org/pub/release-116/fasta/macaca_mulatta/cds/Macaca_mulatta.Mmul_10.cds.all.fa.gz
What dN/dS says
Every coding substitution between two orthologs is either synonymous, changing the codon but not the amino acid, or non-synonymous. Synonymous changes are close to invisible to selection, so their rate dS is roughly the rate at which mutations arrive and fix. Non-synonymous changes are seen, so their rate dN carries whatever selection did.
The ratio is therefore read against 1. Below it, amino acid changes were removed faster than silent ones, which is purifying selection and is where most genes sit most of the time. Above it, amino acid changes fixed faster than silent ones, which takes positive selection to explain.
Producing the data
dS has to be large enough to estimate and small enough not to saturate, and rhesus macaque sits in that window against human. Chimpanzee leaves a denominator near zero on most genes.
Orthologs
The end-to-end script turns each GFF3 into the BED the adapter reads, translates each CDS to a proteome keyed the same way, and runs jcvi:
diamond makedb --in rhesus.pep -d rhesus.pep
diamond blastp --threads 14 --query human.pep --db rhesus.pep \
--out human.rhesus.last --max-target-seqs 20 --evalue 1e-10 --outfmt 6
python -m jcvi.compara.catalog ortholog --no_strip_names --dbtype prot \
--align_soft diamond_blastp --no_dotplot human rhesus
Two traps, both silent.
The alignment file has to be query = the first species, subject = the
second, which is the order jcvi would have used had it run the aligner itself.
Reversed, every id is looked up in the wrong BED and the run ends with
A total of 0 anchor was found.
Ensembl versions transcript ids in its FASTA and not in its GFF3, so the
proteome ends up naming ENST00000641515.7 where the BED names
ENST00000641515. Nothing matches and nothing says so. The script strips the
version; kaks_from_pairs.py takes --strip-version for the same reason.
dN and dS
python3 kaks_from_pairs.py pairs.tsv both.cds.fa.gz \
--key record --strip-version -o kaks.tsv
kaks_from_pairs.py
aligns each pair as protein, back-translates to codons so that nothing shifts
frame, and runs Nei-Gojobori.
Filtering paralogs and low-count pairs
Two species diverged once, so their true orthologs share a divergence time and
their dS values cluster. A pair whose dS comes out an order of magnitude above
that cluster is a paralog the aligner preferred, and the script's --max-ds
removes it.
Sorting the table by dN/dS and reading off the top returns the pairs with almost
nothing to divide by: HBA1, about as strongly conserved as a gene gets, comes
out over 2 off a single synonymous difference, and so do the others near the
top. --min-syn-subs is a floor on that count. dS is per site, so the same rate
is much weaker evidence in a short gene than a long one.
Every row also carries that count and a two-sided Fisher exact p, which is the
test
MEGA
prescribes when the numbers of substitutions are small, where the large-sample
Z-test over-rejects. They are attributeColumns like the rates, so clicking a
link shows how much evidence is under its colour.
Loading the blocks table in JBrowse
The output is a pair table with the two rates after the two gene columns, which
is the .blocks shape
MCScanBlocksAdapter reads:
{
"type": "SyntenyTrack",
"trackId": "primate_orthologs",
"name": "Human / rhesus orthologs (dN/dS)",
"assemblyNames": ["human", "rhesus"],
"adapter": {
"type": "MCScanBlocksAdapter",
"uri": "primate.blocks.gz",
"blockAssemblies": ["human", "rhesus"],
"bedLocations": [{ "uri": "human.bed.gz" }, { "uri": "rhesus.bed.gz" }],
"assemblyNames": ["human", "rhesus"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}
jbrowse add-track-json '{
"type": "SyntenyTrack",
"trackId": "primate_orthologs",
"name": "Human / rhesus orthologs (dN/dS)",
"assemblyNames": ["human", "rhesus"],
"adapter": {
"type": "MCScanBlocksAdapter",
"uri": "primate.blocks.gz",
"blockAssemblies": ["human", "rhesus"],
"bedLocations": [{ "uri": "human.bed.gz" }, { "uri": "rhesus.bed.gz" }],
"assemblyNames": ["human", "rhesus"],
"attributeColumns": ["dn", "ds", "syn_subs", "fisher_p"]
}
}'
attributeColumns names the columns after the two gene columns, so each becomes
a feature attribute visible in the detail panel, and dn with ds together
drive Color by... → dN/dS. syn_subs and fisher_p are what a reader
checks a colour against.
That ramp is fixed, with 1 at its middle and 2 at its top, so a pair's colour says which side of 1 it falls on.
Two settings matter for a view this sparse, and both are properties of the
LinearSyntenyView rather than of the track. alpha defaults to 0.2, tuned for
whole-genome views where thousands of ribbons overlap; at 0.95 the colour is the
colour. drawCurves renders the links as beziers, which separates stacked
neighbours.
Reading the plot
The neighbourhood is collinear, so the ribbons run parallel and colour is the only thing that varies across them. Lysozyme is a good gene to find there: adaptive evolution of primate lysozyme is one of the older results in molecular evolution, the enzyme having been recruited as a digestive protein in foregut fermenters.
Click the orange link and the detail panel gives the count and the p behind it: a handful of synonymous differences, and a Fisher p nowhere near significant. One pairwise comparison carries very little power, and the published result rests on codon models across many primate lineages.
The blue is the colour that tests strongly here: a conserved gene accumulates enough synonymous change to measure while holding non-synonymous change near zero. Across the whole table the great majority of pairs sit significantly below 1 and almost none significantly above.
Checking the rates against the raw data
The figure carries its own control. YEATS4 begins just past where LYZ ends, so the two share a locus, a divergence time and a neighbourhood, and they land at opposite ends of the ramp. YEATS4 is also the pair the substitution-count floor keeps: it is conserved and compact, so its dS is low while its synonymous count is adequate.
The script prints the neighbourhood beside the genome-wide distribution as two counts: how many pairs exceed 1, and how many of those survive the Fisher test. Few do the first, and those that survive the second are about what chance alone would give at that many tests.
Reproduce it end to end
build_primate_selection.sh
runs everything above: it downloads both annotations from Ensembl, builds the
BEDs, proteomes and gene tracks, runs DIAMOND and jcvi, measures dN and dS on
every ortholog pair, prints the tables in the section above, downloads JBrowse,
and writes a config.json with both assemblies, both gene tracks, the ortholog
track and a session opening the locus.
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_primate_selection.sh
bash build_primate_selection.sh
npx --yes serve primate_selection_build/jbrowse2 # then open the printed URL
It needs the tools under Prerequisites on PATH.
See also
- Synteny visualization (a polyploid against itself)
- Synteny from an ortholog table (grape, peach, cacao)
- Synteny from MCScan anchors (grape, peach)
References
- Nei, M. & Gojobori, T. Simple methods for estimating the numbers of synonymous and nonsynonymous nucleotide substitutions. Molecular Biology and Evolution 3, 418-426 (1986). https://doi.org/10.1093/oxfordjournals.molbev.a040410
- Messier, W. & Stewart, C.-B. Episodic adaptive evolution of primate lysozymes. Nature 385, 151-154 (1997). https://doi.org/10.1038/385151a0
- Tang, H. et al. jcvi: A versatile toolkit for comparative genomics analysis. iMeta 3, e211 (2024). https://doi.org/10.1002/imt2.211
Feedback on this tutorial is welcome: contact us.