2.2 The sequence in RNA-seq: variants, selection and ancestry¶
Theme: Analysis
In Module 2.1 we treated RNA-seq reads as counts. But every read is also a piece of sequence. The Busia libraries cover almost every expressed gene at high depth, so we can call SNPs, measure allele frequencies of known resistance mutations, scan for genetic differentiation between groups, estimate genetic diversity, and even infer chromosomal inversions and species ancestry.
In Busia this matters: the strongest single change during deltamethrin selection, the rise of the Vgsc-995S kdr mutation, cannot be seen in the expression data at all. This module shows how RNA-Seq-Pop extracts population genetic signals from RNA-seq, what can go wrong, and what the signals say about the Busia colony and your candidate genes.
Learning objectives¶
At the end of this module you will be able to:
Explain how variants are called from pooled RNA-seq, and the main caveats: uneven coverage, allele-specific expression and pooling.
Read allele frequencies of known resistance mutations (e.g. Vgsc-995S) from RNA-Seq-Pop output.
Interpret Fst and PBS per gene and in genomic windows, and genetic diversity statistics.
Use karyotype and ancestry-informative marker results to check and interpret a study.
Summarise the sequence evidence for your candidate genes.
Setup¶
%pip install -q --no-warn-conflicts plotly==5.24.1import tarfile
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
pio.templates.default = "simple_white" # course style for every Plotly figure
RESULTS_URL = "..." # TODO(data): Zenodo record of RNA-Seq-Pop Busia results (busia-rna-seq-pop-results.tar.gz)
RNASEQPOP_RAW = "https://raw.githubusercontent.com/sanjaynagi/rna-seq-pop/v2.3.0"
# Where the results live. To use your own run in a Codespace, set this to Path("/workspaces/rna-seq-pop")
RESULTS_DIR = Path("busia-results")
if not (RESULTS_DIR / "results").exists():
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
archive = RESULTS_DIR / "busia-rna-seq-pop-results.tar.gz"
urllib.request.urlretrieve(RESULTS_URL, archive)
with tarfile.open(archive) as tar:
tar.extractall(RESULTS_DIR, filter="data")
R = RESULTS_DIR / "results"
VA = R / "variantAnalysis"
samples = pd.read_csv(RESULTS_DIR / "config" / "samples.tsv", sep="\t")
CONTIGS = ["2L", "2R", "3L", "3R", "X"]
CONTRASTS = ["Kisumu_BusiaParental", "BusiaParental_BusiaSelected"]
PBS_CONTRAST = "BusiaSelected_BusiaParental_Kisumu"
TREATMENTS = ["Kisumu", "BusiaParental", "BusiaSelected"]
ORDER = {"treatment": TREATMENTS}
COLOURS = {"Kisumu": "#4c78a8", "BusiaParental": "#f58518", "BusiaSelected": "#e45756"}
# AgamP4 coordinates used for annotation
INVERSIONS = {"2La": ("2L", 20_524_058, 42_165_532), "2Rb": ("2R", 19_023_925, 26_758_676)}
LANDMARK_GENES = {
"AGAP004707": "Vgsc", "AGAP006048": "CYP4J5", "AGAP002867": "CYP6P4", "AGAP002862": "CYP6AA1",
"AGAP009194": "GSTE2", "AGAP008052": "SAP2", "AGAP002865": "CYP6P3", "AGAP000818": "CYP9K1",
}# Replace with the genes you were assigned on Day 2 (see data/candidate-genes.tsv)
CANDIDATE_GENES = ["AGAP002862", "AGAP009193"] # CYP6AA1, GSTE4Calling variants from RNA-seq¶
RNA-Seq-Pop’s variant branch runs in parallel with the expression branch:
HISAT2 aligns reads to the genome. It is splice-aware, so reads that span an intron are split across the two exons. Duplicate reads are marked with samblaster.
Freebayes calls SNPs jointly in all 16 samples, treating each group as a population, with
--ploidy 10and--pooled-discrete: each library is a pool of 5 diploid females, so it is modelled as one “individual” with 10 genome copies.Indels are removed (they are unreliable from RNA-seq), and snpEff annotates each SNP (synonymous, missense, intronic ...).
For each analysis, SNPs are filtered on quality (QUAL ≥ 30) and missingness. In our config
missingness: 1keeps only SNPs with genotype calls in every sample.
A genotype call like 0/0/0/0/0/0/0/1/1/1 means the pool is estimated to carry 3 alternative
alleles out of 10, an allele frequency of 0.3.
Caveats¶
RNA-seq is not whole-genome sequencing. Keep these in mind whenever you interpret variant results:
Only expressed regions are covered. Depth is proportional to expression, so a highly expressed gene may have 1,000× coverage and a lowly expressed gene almost none. Genes not expressed in adult females (or in whole bodies) contribute no SNPs. Most intergenic and intronic sequence is missing.
Allele-specific expression (ASE). If one allele is transcribed more than the other (for example because a cis-regulatory variant is linked to it), its frequency in the RNA is higher than in the DNA. For a gene overexpressed through a linked promoter variant or duplication, this bias is exactly where you are looking. Read-based allele frequencies are “expressed allele frequencies”.
Pooling. Pools of 5 give at most 10 chromosomes per library, and individuals may not contribute equal amounts of RNA. You cannot recover individual genotypes or haplotypes.
Mapping artefacts. Reads near splice junctions, from paralogous genes (e.g. P450 clusters) or from RNA editing sites can create false SNPs.
Replicates are not independent populations. In a colony, replicates are drawn from the same population, so between-replicate variation reflects sampling of mosquitoes, not populations.
Despite these, allele frequencies from well-covered RNA-seq correlate well with DNA-based estimates [1]. The paper recommends 25–60 million reads per library and ≥ 10 individuals per pool for variant work.
First, where are the SNPs? RNA-Seq-Pop compares the proportion of SNPs in each genomic feature with the proportion of the reference genome made up by that feature:
features = pd.read_csv(VA / "SNPstats" / "snpsPerGenomicFeature.tsv", sep="\t", index_col=0)
features = features.loc[features.index.intersection(["exon", "intron", "intergenic", "five_prime_UTR", "three_prime_UTR"])]
plot_df = features[["ReferenceGenomeProportion", "proportion"]].rename(
columns={"ReferenceGenomeProportion": "reference genome", "proportion": "Busia SNPs"}
).rename_axis("feature").reset_index().melt(id_vars="feature", var_name="", value_name="proportion")
fig = px.bar(plot_df, x="feature", y="proportion", color="", barmode="group",
title="SNPs from RNA-seq are concentrated in exons", template="simple_white")
fig.show()
print(pd.read_csv(VA / "SNPstats" / "totalSNPs.tsv", sep="\t", index_col=0))Variants of interest¶
For known mutations we do not need freebayes at all. RNA-Seq-Pop runs samtools mpileup at each
position listed in resources/exampleMutations.tsv and counts the reads carrying each base in each
BAM. The allele balance (proportion of reads with the mutant base) estimates the expressed allele
frequency. The list includes the kdr mutations in Vgsc (the voltage-gated sodium channel, target of
pyrethroids and DDT), Cyp4j5-L43F, Cyp6p4-I236M, Gste2-I114T and L119V, Ace1-G280S and
Rdl-A296G/S.
Vgsc mutations are named by their position in the Musca domestica protein in older papers; in An. gambiae numbering, the East African kdr mutation L1014S is L995S and the West African L1014F is L995F.
voi = pd.read_csv(f"{RNASEQPOP_RAW}/resources/exampleMutations.tsv", sep="\t")
voiCSV_DIR = VA / "variantsOfInterest" / "csvs"
per_treatment, per_sample = [], []
for _, mut in voi.iterrows():
label = f"{mut.Gene} {mut.Name}"
mean_df = pd.read_csv(CSV_DIR / f"mean_{mut.Name}_alleleBalance.csv")
mean_df["frequency"] = mean_df.filter(like="proportion").sum(axis=1)
mean_df["variant"] = label
per_treatment.append(mean_df[["variant", "treatment", "frequency", "cov_mean", "lowerCI", "upperCI"]])
sample_df = pd.read_csv(CSV_DIR / f"{mut.Name}_alleleBalance.csv")
sample_df["frequency"] = sample_df.filter(like="proportion").sum(axis=1)
sample_df["variant"] = label
per_sample.append(sample_df[["variant", "sample", "treatment", "frequency", "cov"]])
per_treatment = pd.concat(per_treatment)
per_sample = pd.concat(per_sample)
heat = per_treatment.pivot_table(index="variant", columns="treatment", values="frequency")[TREATMENTS]
fig = px.imshow(heat, text_auto=".2f", zmin=0, zmax=1, color_continuous_scale="Reds", aspect="auto",
title="Expressed allele frequency of variants of interest", height=650)
fig.show()# Read depth matters: how many reads cover each variant, per group?
per_treatment.pivot_table(index="variant", columns="treatment", values="cov_mean")[TREATMENTS].round(0)# Per-sample frequencies for the main variants
focus = ["VGSC L995S", "VGSC L995F", "CYP4J5 L43F", "GSTe2 L119V", "GSTe2 I114T", "CYP6P4 I236M"]
fig = px.strip(
per_sample[per_sample["variant"].isin(focus)], x="treatment", y="frequency", color="treatment",
facet_col="variant", facet_col_wrap=3, color_discrete_map=COLOURS, category_orders=ORDER,
hover_data=["sample", "cov"], template="simple_white", height=550,
)
fig.update_yaxes(range=[-0.05, 1.05])
fig.show()In the paper [2], Vgsc-995S rose from 25% in the parental line to 100% in the selected survivors, and Cyp4j5-L43F from 45% to 95%. Gste2-L119V rose from 11% to 52%, while Cyp6p4-I236M did not change (39% vs 38%). Gste2-I114T was absent from Busia but at 92% in Kisumu, a surprise for a “susceptible” strain. No secondary kdr mutations were found alongside 995S.
Genetic differentiation: Fst¶
Fst measures how much of the genetic variation lies between populations rather than within them. RNA-Seq-Pop uses Hudson’s Fst [3], computed from allele counts with scikit-allel:
where is the mean diversity within the two populations and the diversity between them. Fst is 0 if the two groups have identical allele frequencies and 1 if they are fixed for different alleles.
Selection on a locus raises Fst there, because the selected allele (and the haplotype around it) changes frequency more than the genomic background. But genetic drift also raises Fst, especially in small lab colonies, so the genome-wide background matters: we look for genes and regions that stand out.
Fst per gene¶
results/variantAnalysis/selection/FstPerGene.tsv has, for each gene with at least 2 SNPs, the
average Fst for each contrast ({contrast}_zFst, despite the name it is not a z-score), its
standard error, the number of SNPs and the gene midpoint.
fst = pd.read_csv(VA / "selection" / "FstPerGene.tsv", sep="\t")
for c in CONTRASTS:
fst[f"{c}_percentile"] = fst[f"{c}_zFst"].rank(pct=True) * 100
fst.head()def manhattan(df, value_col, title, labels=LANDMARK_GENES):
df = df.dropna(subset=[value_col]).copy()
df["label"] = df["GeneID"].map(labels)
fig = px.scatter(
df, x="Gene_midpoint", y=value_col, facet_col="contig", color="contig",
category_orders={"contig": CONTIGS}, hover_data=["GeneID", "GeneName", "nSNPs"],
opacity=0.5, template="simple_white", height=400, title=title,
)
fig.update_xaxes(matches=None, title="")
fig.update_layout(showlegend=False)
for i, contig in enumerate(CONTIGS, start=1):
lab = df[(df["contig"] == contig) & df["label"].notna()]
fig.add_trace(go.Scatter(x=lab["Gene_midpoint"], y=lab[value_col], text=lab["label"],
mode="markers+text", textposition="top center",
marker=dict(color="black", size=7), showlegend=False),
row=1, col=i)
return fig
manhattan(fst, "BusiaParental_BusiaSelected_zFst", "Fst per gene: BusiaParental vs BusiaSelected").show()# Where do the landmark genes rank? (paper: Vgsc and Cyp4j5 in the top 5%, Cyp6p4 at the 89th percentile)
fst[fst["GeneID"].isin(LANDMARK_GENES)][
["GeneID", "GeneName", "contig", "nSNPs", "BusiaParental_BusiaSelected_zFst", "BusiaParental_BusiaSelected_percentile"]
].round(3)Fst in windows¶
Per-gene Fst is noisy for genes with few SNPs. A genome-wide selection scan averages Fst over
windows of consecutive SNPs instead. RNA-Seq-Pop writes windows of 1,000, 2,000 and 5,000 SNPs to
results/variantAnalysis/selection/fst/{window}/{contrast}.Fst.{contig}.tsv (columns midpoint,
Fst). Because SNPs from RNA-seq are clustered in genes, a 1,000-SNP window can span anything from a
few kb to several Mb.
def load_windows(stat, contrast, window="1000snp_window"):
frames = []
for contig in CONTIGS:
path = VA / "selection" / stat.lower() / window / f"{contrast}.{stat}.{contig}.tsv"
if path.exists():
df = pd.read_csv(path, sep="\t")
df["contig"] = contig
frames.append(df)
return pd.concat(frames, ignore_index=True)
def plot_windows(df, stat, title):
fig = px.line(df, x="midpoint", y=stat, facet_col="contig", category_orders={"contig": CONTIGS},
template="simple_white", height=380, title=title)
fig.update_xaxes(matches=None, title="")
for name, (contig, start, end) in INVERSIONS.items():
col = CONTIGS.index(contig) + 1
fig.add_vrect(x0=start, x1=end, fillcolor="grey", opacity=0.15, line_width=0, row=1, col=col,
annotation_text=name, annotation_position="top left")
fig.add_vline(x=2_390_000, line_dash="dot", line_color="red", row=1, col=1) # Vgsc
return fig
fst_win = {c: load_windows("Fst", c) for c in CONTRASTS}
plot_windows(fst_win["BusiaParental_BusiaSelected"], "Fst",
"Windowed Fst (1,000 SNPs), BusiaParental vs BusiaSelected; red line = Vgsc").show()plot_windows(fst_win["Kisumu_BusiaParental"], "Fst", "Windowed Fst (1,000 SNPs), Kisumu vs BusiaParental").show()# Average Fst per chromosome arm
pd.DataFrame({c: w.groupby("contig")["Fst"].mean() for c, w in fst_win.items()}).round(3)The paper found high Fst between the parental and selected lines across the genome (a small colony under strong selection drifts fast), but especially on 2L, with peaks around Vgsc and across the 2La inversion.
The population branch statistic (PBS)¶
Fst between two groups cannot tell you which group changed. PBS [4] uses three groups. Each pairwise Fst is turned into a branch length , and the branch leading to population A is
With BusiaSelected_BusiaParental_Kisumu, PBS is the amount of change specific to the selected
line since it split from the parental line, with Kisumu as the outgroup. RNA-Seq-Pop uses
scikit-allel’s normalised PBS, which reduces the influence of loci where all branches are long.
Per-gene values are in results/variantAnalysis/selection/PbsPerGene.tsv, windows in
results/variantAnalysis/selection/pbs/.
pbs = pd.read_csv(VA / "selection" / "PbsPerGene.tsv", sep="\t")
pbs_col = f"{PBS_CONTRAST}PBS"
pbs["percentile"] = pbs[pbs_col].rank(pct=True) * 100
manhattan(pbs, pbs_col, "PBS per gene: BusiaSelected branch").show()# The genes with the highest PBS (at least 10 SNPs, to avoid noisy genes)
pbs.query("nSNPs >= 10").nlargest(20, pbs_col)[["GeneID", "GeneName", "GeneDescription", "contig", "nSNPs", pbs_col]].round(3)pbs_win = load_windows("PBS", PBS_CONTRAST)
plot_windows(pbs_win, "PBS", "Windowed PBS (1,000 SNPs), BusiaSelected branch").show()Worked example: the Cyp6aa/Cyp6p cluster¶
In Module 2.1 we looked at the expression of CYP6AA1 (AGAP002862). Now we look at the sequence
around it. Cyp6aa1 sits in a cluster of P450 genes on 2R (~28.45–28.55 Mb) that includes Cyp6p3
and Cyp6p4. In Uganda and Kenya a triple-mutant haplotype (a Cyp6aa1 duplication, the
Cyp6p4-I236M missense mutation and a ZZB transposable element insertion) has risen rapidly and is
associated with pyrethroid resistance [9]. If selection in the colony acted on this haplotype, we
would expect raised Fst and PBS in the cluster and a change in Cyp6p4-I236M frequency.
Recall from the variants-of-interest heatmap that the paper found no change in Cyp6p4-I236M between the parental and selected lines (39% vs 38%), and that Cyp6p4 was only at the 89th percentile of per-gene Fst. Let’s check the whole cluster.
CLUSTER = ("2R", 28_400_000, 28_600_000)
contig, start, end = CLUSTER
cluster = (
fst.query("contig == @contig and @start <= Gene_midpoint <= @end")
.merge(pbs[["GeneID", pbs_col, "percentile"]].rename(columns={"percentile": "PBS_percentile"}), how="left")
.sort_values("Gene_midpoint")
)
cluster[["GeneID", "GeneName", "Gene_midpoint", "nSNPs", "BusiaParental_BusiaSelected_zFst",
"BusiaParental_BusiaSelected_percentile", pbs_col, "PBS_percentile"]].round(3)# Windowed Fst and PBS on 2R, zoomed in around the cluster
zoom = pd.concat([
fst_win["BusiaParental_BusiaSelected"].assign(statistic="Fst BusiaParental vs BusiaSelected").rename(columns={"Fst": "value"}),
pbs_win.assign(statistic="PBS BusiaSelected branch").rename(columns={"PBS": "value"}),
]).query("contig == '2R' and 24_000_000 <= midpoint <= 33_000_000")
fig = px.line(zoom, x="midpoint", y="value", facet_row="statistic", markers=True,
template="simple_white", height=500, title="Chromosome 2R around the Cyp6aa/Cyp6p cluster")
fig.update_yaxes(matches=None)
fig.add_vrect(x0=start, x1=end, fillcolor="red", opacity=0.15, line_width=0,
annotation_text="Cyp6aa/p", annotation_position="top left")
fig.show()Duplications and RNA-seq. A duplicated gene has more copies to transcribe and more genomic template for reads to map to. In RNA-seq this inflates the gene’s expression estimate, and in the HISAT2 alignments it shows up as elevated coverage over the duplicated region compared with its neighbours. But RNA-seq coverage also depends on how strongly each gene is expressed, so from RNA-seq alone you cannot separate “more copies” from “more transcription per copy”. Duplications are best detected from whole-genome coverage, which is what you will do for wild Busia mosquitoes on Day 3.
If you ran RNA-Seq-Pop in your Codespace, you can look at the coverage yourself (in the Codespace
terminal, from /workspaces/rna-seq-pop):
# mean depth per sample over Cyp6aa1 (2R:28,480,576-28,482,637) and its neighbour Cyp6p4
for bam in results/alignments/*.hisat2.bam; do
echo -n "$(basename $bam .hisat2.bam) "
samtools depth -a -r 2R:28480576-28482637 $bam | awk '{s+=$3} END {printf "Cyp6aa1 %.0f ", s/NR}'
samtools depth -a -r 2R:28497000-28499000 $bam | awk '{s+=$3} END {printf "Cyp6p4 %.0f\n", s/NR}'
done(samtools is in RNA-Seq-Pop’s variants conda environment under .snakemake/conda/.)
Genetic diversity¶
Nucleotide diversity (π) is the average number of differences per site between two randomly
chosen sequences. Watterson’s θ is based on the number of segregating sites. Both fall after a
population bottleneck (e.g. colonisation) and around a selective sweep. RNA-Seq-Pop averages them
over 20 kb windows (results/variantAnalysis/diversity/SequenceDiversity.tsv and
WattersonsTheta.tsv), and also gives per-gene π and Tajima’s D.
Because SNPs are called only in expressed regions and windows with no data count as zero, the absolute values are not comparable with WGS estimates. Compare groups with each other.
pi = pd.read_csv(VA / "diversity" / "SequenceDiversity.tsv", sep="\t", index_col=0)
theta = pd.read_csv(VA / "diversity" / "WattersonsTheta.tsv", sep="\t", index_col=0)
div = pd.concat({"π": pi, "Watterson's θ": theta}, names=["statistic", "treatment"]).reset_index()
div["error"] = div["ci95_hi"] - div["mean"]
fig = px.bar(div, x="treatment", y="mean", error_y="error", color="treatment", facet_col="statistic",
color_discrete_map=COLOURS, category_orders=ORDER, template="simple_white", height=400,
title="Genome-wide diversity (mean of 20 kb windows, 95% CI)")
fig.update_yaxes(matches=None, showticklabels=True)
fig.show()In the paper, the parental Busia line had the highest diversity, the selected line less, and Kisumu the least (π ≈ 1.0 × 10⁻³, 0.7 × 10⁻³ and 0.6 × 10⁻³). Note that the paper down-sampled the Busia groups to four replicates to match Kisumu, whereas our run uses all six, which can shift the estimates slightly.
# Inbreeding coefficient (genome-wide mean), from the same notebook
pd.read_csv(VA / "diversity" / "inbreedingCoef.mean.tsv", sep="\t", index_col=0).round(3)Karyotyping chromosomal inversions¶
An. gambiae carries large polymorphic inversions on chromosome 2. 2La (~21 Mb) is associated
with aridity tolerance, Plasmodium infection and insecticide resistance. Inside an inversion,
recombination between the inverted and standard arrangements is suppressed, so some SNPs are almost
perfectly associated with each arrangement. RNA-Seq-Pop uses these tag SNPs (from compkaryo [5])
and reports, for each sample, the mean number of inverted alleles at the tag SNPs it could genotype
(_mean, out of the ploidy of 10), the karyotype frequency (_freq) and the number of tag SNPs used.
karyo = pd.read_csv(R / "karyotype" / "karyotypes.tsv", sep="\t", index_col="sample_id")
karyo = karyo.merge(samples, left_index=True, right_on="sampleID")
display(karyo)
freq_cols = [c for c in karyo.columns if c.endswith("_freq")]
long = karyo.melt(id_vars=["sampleID", "treatment"], value_vars=freq_cols,
var_name="inversion", value_name="frequency")
long["inversion"] = long["inversion"].str.replace("karyotype_", "").str.replace("_freq", "")
fig = px.box(long, x="treatment", y="frequency", color="treatment", facet_col="inversion", points="all",
color_discrete_map=COLOURS, category_orders=ORDER, hover_data=["sampleID"],
template="simple_white", height=400, title="Inversion karyotype frequency")
fig.update_yaxes(range=[0, 1])
fig.show()The paper found 2La at 33% in the parental line and 86% in the selected survivors, supporting an earlier association between 2La and pyrethroid resistance in Uganda. 2Rb differed between Kisumu and both Busia lines.
This has a direct consequence for Fst: the whole 2La region (20.5–42.2 Mb on 2L) changed frequency as one block, so every gene inside it shows raised Fst. Cyp4j5 sits inside 2La. Is Cyp4j5-L43F itself selected, or is it hitchhiking on the inversion?
Ancestry-informative markers¶
The An. gambiae complex contains closely related species that can hybridise. Ancestry-informative markers (AIMs) are SNPs with (nearly) fixed differences between species. RNA-Seq-Pop counts, for each group and chromosome arm, the fraction of alleles at AIMs that match An. gambiae or An. coluzzii.
aims = []
for contig in CONTIGS:
df = pd.read_csv(VA / "ancestry" / f"AIM_fraction_{contig}.tsv", sep="\t", index_col=0)
df["contig"] = contig
aims.append(df.rename_axis("treatment").reset_index())
aims = pd.concat(aims).melt(id_vars=["treatment", "contig"], var_name="ancestry", value_name="fraction")
aims["ancestry"] = aims["ancestry"].str.replace("AIM_fraction_", "An. ")
fig = px.bar(aims, x="treatment", y="fraction", color="ancestry", facet_col="contig",
category_orders={"treatment": TREATMENTS, "contig": CONTIGS},
color_discrete_map={"An. gambiae": "#e45756", "An. coluzzii": "#4c78a8"},
template="simple_white", height=400, title="AIM fraction per chromosome arm")
fig.show()
print(pd.read_csv(VA / "ancestry" / "n_AIMS_per_chrom.tsv", sep="\t", index_col=0))The paper found that the Busia colony is almost entirely An. gambiae, but Kisumu shows a large
amount of putative An. coluzzii ancestry on the autosomes and none on X. Kisumu was colonised in
1975 from an area without An. coluzzii, so the most likely explanation is contamination by
hybridisation in an insectary. The X chromosome resists introgression, which fits this. This is a
good example of RNA-seq variants acting as a quality check on the experiment itself; it is also a
reason to be cautious about Kisumu as a “reference” in the Kisumu_BusiaParental contrast.
Your candidate genes¶
Collect the sequence evidence for your genes: number of SNPs, Fst in each contrast (and its percentile), PBS, and diversity and Tajima’s D in each group. Is the gene inside 2La or 2Rb?
tajd = pd.read_csv(VA / "selection" / "TajimasDPerGene.tsv", sep="\t")
seqdiv = pd.read_csv(VA / "diversity" / "SequenceDivPerGene.tsv", sep="\t")
table = (
fst[["GeneID", "GeneName", "contig", "Gene_midpoint", "nSNPs"]
+ [f"{c}_zFst" for c in CONTRASTS] + [f"{c}_percentile" for c in CONTRASTS]]
.merge(pbs[["GeneID", pbs_col, "percentile"]].rename(columns={"percentile": "PBS_percentile"}), how="left")
.merge(tajd.filter(regex="GeneID|_Tajima_d"), how="left")
.merge(seqdiv.filter(regex="GeneID|_SeqDiv"), how="left")
)
def in_inversion(row):
for name, (contig, start, end) in INVERSIONS.items():
if row["contig"] == contig and start <= row["Gene_midpoint"] <= end:
return name
return ""
table["inversion"] = table.apply(in_inversion, axis=1)
mine = table[table["GeneID"].isin(CANDIDATE_GENES)].set_index("GeneID").T
mineIf a gene is missing from the table, it had fewer than 2 SNPs passing the filters: usually because it is lowly expressed (little coverage) or not variable. That in itself is worth noting in your dossier.
Exercise 1¶
Which genes have the highest PBS on the BusiaSelected branch? Take the top 20 genes with at least 10 SNPs and look them up: are any known resistance genes, and how many are inside 2La?
Show answer
top = table.query("nSNPs >= 10").nlargest(20, pbs_col)
top[["GeneID", "GeneName", "contig", "Gene_midpoint", pbs_col, "inversion"]]
top["inversion"].value_counts()Expect Vgsc and genes on 2L to feature. If many top genes lie inside 2La, the PBS signal is dominated by the change in inversion frequency, and you cannot tell which gene inside it (if any) is the target of selection from these data alone.
Exercise 2¶
Compare Vgsc (AGAP004707) in the two kinds of analysis: its differential expression result
(Module 2.1: results/genediff/BusiaParental_BusiaSelected.csv) and its Fst percentile here. Now do
the same for SAP2 (AGAP008052). What does each gene’s pattern suggest about the mechanism?
Show answer
de = pd.read_csv(R / "genediff" / "BusiaParental_BusiaSelected.csv")
de[de.GeneID.isin(["AGAP004707", "AGAP008052"])][["GeneID", "GeneName", "log2FoldChange", "padj"]]
table[table.GeneID.isin(["AGAP004707", "AGAP008052"])][["GeneID", "nSNPs", "BusiaParental_BusiaSelected_percentile"]]Vgsc: not differentially expressed, but high Fst, driven by the rise of 995S: target-site resistance through a change in the protein. SAP2: strongly overexpressed, and the paper found no non-synonymous variants in it: a change in expression (through cis or trans regulation, or induction by exposure). The two analyses find different mechanisms.
Exercise 3¶
The Cyp4j5-L43F frequency in the RNA of a pool may not equal its frequency in the DNA. Give two reasons, and say which direction each would bias the estimate.
Show answer
Allele-specific expression. If the 43F allele sits on a haplotype (e.g. the 2La arrangement) that expresses Cyp4j5 more highly, 43F reads are over-represented and the frequency is overestimated. The reverse is also possible.
Unequal RNA from each mosquito in a pool. A larger or more transcriptionally active female contributes more reads, biasing the frequency towards her genotype (either direction).
Others: low coverage (sampling noise), mapping bias against the non-reference allele (underestimate), and the fact that selected samples were survivors of exposure, which inflates resistance allele frequencies compared with the unexposed G28 population.
Exercise 4¶
Fill in the “Genomic variation” part of your dossier for each candidate gene: number of SNPs, Fst percentile in both contrasts, PBS percentile, inversion, and any variant of interest in or near the gene. On Day 3 you will check whether the same genes carry CNVs or missense SNPs in wild Ugandan and Kenyan mosquitoes.
Show answer
Use the mine table above. A strong case for selection in the colony would be a gene with enough SNPs (say ≥ 10), Fst and PBS in the top 5%, and a signal that is not simply explained by lying inside 2La. A gene with no SNPs is not evidence against selection; it may just be poorly covered.
Quiz¶
Check your understanding. The quiz runs in Colab and in the course book.
%pip install -q jupyterquiz==2.9.6.4
from jupyterquiz import display_quiz
display_quiz("https://raw.githubusercontent.com/sanjaynagi/anopheles-omics-training/main/quizzes/2-2-sequence-in-rna-seq.json")Summary¶
RNA-seq reads give SNPs in expressed genes. RNA-Seq-Pop aligns with HISAT2 and calls pooled variants with freebayes (ploidy 10 for pools of 5 diploids).
Expressed allele frequencies can be biased by allele-specific expression, uneven coverage and pooling, but are useful when coverage is high.
In Busia, selection raised Vgsc-995S to 100% and Cyp4j5-L43F to 95%, and the 2La inversion from 33% to 86%. Fst was highest on 2L.
Fst measures differentiation between two groups; PBS isolates change on one branch using an outgroup. Inversions and drift in small colonies raise both, so compare against the background.
Karyotypes and AIMs are checks on the experiment: Kisumu carries An. coluzzii ancestry on its autosomes.
Well done!¶
You have now combined expression and sequence evidence for your candidate genes. In Module 2.3 you check whether they are overexpressed in other studies with AnoExpress.
References¶
Jehl F et al. (2021). RNA-Seq data for reliable SNP detection and genotype calling: interest for coding variant characterization and cis-regulation analysis by allele-specific expression in livestock species. Frontiers in Genetics 12:655707. Jehl et al. (2021)
Nagi SC, Oruni A, Weetman D, Donnelly MJ (2023). RNA-Seq-Pop: Exploiting the sequence in RNA sequencing—A Snakemake workflow reveals patterns of insecticide resistance in the malaria vector Anopheles gambiae. Molecular Ecology Resources 23:946–961. Nagi et al. (2023)
Bhatia G, Patterson N, Sankararaman S, Price AL (2013). Estimating and interpreting FST: the impact of rare variants. Genome Research 23:1514–1521. https://
doi .org /10 .1101 /gr .154831 .113 Yi X et al. (2010). Sequencing of 50 human exomes reveals adaptation to high altitude. Science 329:75–78. Yi et al. (2010)
Love RR, Redmond SN, Pombi M, Caputo B, Petrarca V, della Torre A, Besansky NJ (2019). In silico karyotyping of chromosomally polymorphic malaria mosquitoes in the Anopheles gambiae complex. G3 9:3249–3262. Love et al. (2019)
Clarkson CS et al. (2021). The genetic architecture of target-site resistance to pyrethroid insecticides in the African malaria vectors Anopheles gambiae and Anopheles coluzzii. Molecular Ecology 30:5303–5317. Clarkson et al. (2021)
Weetman D et al. (2018). Candidate-gene based GWAS identifies reproducible DNA markers for metabolic pyrethroid resistance from standing genetic variation in East African Anopheles gambiae. Scientific Reports 8:2920. Weetman et al. (2018)
Miles A et al. scikit-allel: explore and analyse genetic variation. Alistair Miles et al. (2024)
Njoroge H et al. (2022). Identification of a rapidly-spreading triple mutant for high-level metabolic insecticide resistance in Anopheles gambiae provides a real-time molecular diagnostic for antimalarial intervention deployment. Molecular Ecology 31:4307–4318. Njoroge et al. (2022)
- Jehl, F., Degalez, F., Bernard, M., Lecerf, F., Lagoutte, L., Désert, C., Coulée, M., Bouchez, O., Leroux, S., Abasht, B., Tixier-Boichard, M., Bed’hom, B., Burlot, T., Gourichon, D., Bardou, P., Acloque, H., Foissac, S., Djebali, S., Giuffra, E., … Lagarrigue, S. (2021). RNA-Seq Data for Reliable SNP Detection and Genotype Calling: Interest for Coding Variant Characterization and Cis-Regulation Analysis by Allele-Specific Expression in Livestock Species. Frontiers in Genetics, 12. 10.3389/fgene.2021.655707
- Nagi, S. C., Oruni, A., Weetman, D., & Donnelly, M. J. (2023). RNA‐Seq‐Pop : Exploiting the sequence in <scp>RNA</scp> sequencing—A Snakemake workflow reveals patterns of insecticide resistance in the malaria vector Anopheles gambiae. Molecular Ecology Resources, 23(4), 946–961. 10.1111/1755-0998.13759
- Yi, X., Liang, Y., Huerta-Sanchez, E., Jin, X., Cuo, Z. X. P., Pool, J. E., Xu, X., Jiang, H., Vinckenbosch, N., Korneliussen, T. S., Zheng, H., Liu, T., He, W., Li, K., Luo, R., Nie, X., Wu, H., Zhao, M., Cao, H., … Wang, J. (2010). Sequencing of 50 Human Exomes Reveals Adaptation to High Altitude. Science, 329(5987), 75–78. 10.1126/science.1190371
- Love, R. R., Redmond, S. N., Pombi, M., Caputo, B., Petrarca, V., della Torre, A., & Besansky, N. J. (2019). In Silico Karyotyping of Chromosomally Polymorphic Malaria Mosquitoes in the Anopheles gambiae Complex. G3 Genes|Genomes|Genetics, 9(10), 3249–3262. 10.1534/g3.119.400445
- Clarkson, C. S., Miles, A., Harding, N. J., O’Reilly, A. O., Weetman, D., Kwiatkowski, D., & Donnelly, M. J. (2021). The genetic architecture of target‐site resistance to pyrethroid insecticides in the African malaria vectors Anopheles gambiae and Anopheles coluzzii. Molecular Ecology, 30(21), 5303–5317. 10.1111/mec.15845
- Weetman, D., Wilding, C. S., Neafsey, D. E., Müller, P., Ochomo, E., Isaacs, A. T., Steen, K., Rippon, E. J., Morgan, J. C., Mawejje, H. D., Rigden, D. J., Okedi, L. M., & Donnelly, M. J. (2018). Candidate-gene based GWAS identifies reproducible DNA markers for metabolic pyrethroid resistance from standing genetic variation in East African Anopheles gambiae. Scientific Reports, 8(1). 10.1038/s41598-018-21265-5
- Alistair Miles, pyup.io bot, Murillo F. Rodrigues, Peter Ralph, Jerome Kelleher, Max Schelker, Rahul Pisupati, Summer Rae, & Tim Millar. (2024). cggh/scikit-allel: v1.3.13. Zenodo. 10.5281/ZENODO.597309
- Njoroge, H., van’t Hof, A., Oruni, A., Pipini, D., Nagi, S. C., Lynd, A., Lucas, E. R., Tomlinson, S., Grau‐Bove, X., McDermott, D., Wat’senga, F. T., Manzambi, E. Z., Agossa, F. R., Mokuba, A., Irish, S., Kabula, B., Mbogo, C., Bargul, J., Paine, M. J. I., … Donnelly, M. J. (2022). Identification of a rapidly‐spreading triple mutant for high‐level metabolic insecticide resistance in Anopheles gambiae provides a real‐time molecular diagnostic for antimalarial intervention deployment. Molecular Ecology, 31(16), 4307–4318. 10.1111/mec.16591