WithinHostExperiment is a Bioconductor infrastructure
package for analysing intra-host single nucleotide variants (iSNV) from
pathogen deep sequencing data.
Researchers performing within-host pathogen diversity analysis face a fragmented tooling landscape. Variant callers such as iVar, LoFreq, and Freebayes produce outputs in different formats. Quality control relies on ad hoc scripts. There is no standard Bioconductor container that organises within-host variant data with replicate information, donor-recipient pairing, and QC provenance.
WithinHostExperiment fills the infrastructure gap
between upstream variant callers and downstream analysis tools:
RangedSummarizedExperiment-based container| Tool | Role | Relationship |
|---|---|---|
| iVar, LoFreq, Freebayes | Variant calling | Upstream – we read output |
| deepSNV | Low-frequency variant detection | Upstream – complementary |
| ViralBottleneck | Bottleneck estimation | Downstream – simplified frame |
| QSutils | Quasispecies diversity | Parallel – different granularity |
This package belongs in Bioconductor rather than CRAN for several reasons:
WithinHostExperiment extends
RangedSummarizedExperiment and interoperates with
VRanges, GRanges, and the
VariantAnnotation ecosystem.library(WithinHostExperiment)
# Import iSNV data from VCF
vcf <- system.file("extdata", "test_donor.vcf",
package = "WithinHostExperiment")
whe <- readWithinHost(vcf,
colData = S4Vectors::DataFrame(sample_id = "donor"),
caller = "ivar")
whe
#> WithinHostExperiment with 15 variant sites and 1 samples
#> assays(5): altFreq, totalDepth, refCount, altCount, qcPass
#> qcLog: 0 filtering step(s)
#> transmission pairs: 0All core functions take a WithinHostExperiment as their
first argument, making them compatible with R’s native pipe:
library(GenomicRanges)
primers <- GRanges("test_segment", IRanges::IRanges(
start = c(40, 880), end = c(65, 950)))
result <- whe |>
flagISNV(ISNVFilter(minDepth = 200L, minFreq = 0.03)) |>
flagPrimerSites(primers) |>
passedISNV()
cat("Input:", nrow(whe), "sites ->",
"After QC + primer masking:", nrow(result), "sites\n")
#> Input: 15 sites -> After QC + primer masking: 11 sitesEach VCF file corresponds to one sample. Sample metadata is provided
via colData:
vcf1 <- system.file("extdata", "test_donor.vcf",
package = "WithinHostExperiment")
vcf2 <- system.file("extdata", "test_recipient.vcf",
package = "WithinHostExperiment")
whe <- readWithinHost(
c(vcf1, vcf2),
colData = S4Vectors::DataFrame(
sample_id = c("donor", "recipient"),
host_id = c("P1", "P2"),
role = c("donor", "recipient"),
pair_id = c("pair_1", "pair_1")
),
caller = "ivar"
)
whe
#> WithinHostExperiment with 20 variant sites and 2 samples
#> assays(5): altFreq, totalDepth, refCount, altCount, qcPass
#> qcLog: 0 filtering step(s)
#> transmission pairs: 1tsv <- system.file("extdata", "test_donor_ivar.tsv",
package = "WithinHostExperiment")
whe_tsv <- readWithinHostTable(tsv, format = "ivar")
whe_tsv
#> WithinHostExperiment with 15 variant sites and 1 samples
#> assays(5): altFreq, totalDepth, refCount, altCount, qcPass
#> qcLog: 0 filtering step(s)
#> transmission pairs: 0The container extends RangedSummarizedExperiment:
# Assays: variant-level matrices (rows = sites, columns = samples)
SummarizedExperiment::assayNames(whe)
#> [1] "altFreq" "totalDepth" "refCount" "altCount" "qcPass"
# rowRanges: genomic coordinates with ref/alt
head(SummarizedExperiment::rowRanges(whe))
#> GRanges object with 6 ranges and 2 metadata columns:
#> seqnames ranges strand | ref alt
#> <Rle> <IRanges> <Rle> | <character> <character>
#> [1] test_segment 45 * | C T
#> [2] test_segment 78 * | A C
#> [3] test_segment 112 * | A T
#> [4] test_segment 155 * | A G
#> [5] test_segment 198 * | A G
#> [6] test_segment 267 * | C A
#> -------
#> seqinfo: 1 sequence from an unspecified genome; no seqlengths
# colData: sample metadata
SummarizedExperiment::colData(whe)
#> DataFrame with 2 rows and 4 columns
#> sample_id host_id role pair_id
#> <character> <character> <character> <character>
#> donor donor P1 donor pair_1
#> recipient recipient P2 recipient pair_1
# Transmission pairs (from colData role + pair_id)
transmissionPairs(whe)
#> DataFrame with 1 row and 3 columns
#> donor recipient pair_id
#> <character> <character> <character>
#> 1 donor recipient pair_1Accurate iSNV identification requires careful quality control. Roder
et al. (2023, mBio) demonstrated that choice of variant caller
and use of replicate sequencing have the most significant impact on iSNV
discovery. WithinHostExperiment addresses both: it
harmonises output from multiple callers (iVar, LoFreq, Freebayes) and
provides replicate-aware QC as a first-class feature.
flagISNV() marks variants that fail quality criteria. It
does not delete rows – it sets qcPass to
FALSE. This preserves all data for potential re-analysis
with different thresholds.
# Create a filter
filt <- ISNVFilter(minDepth = 200L, minFreq = 0.03)
filt
#> ISNVFilter object
#> minDepth: 200
#> freq range: [0.03, 0.5]
#> minAltReads: 10
#> maxStrandBias: 10
#> minBaseQual: 20
#> minMapQual: 20
#> replicateConc: FALSE
# Apply (flag, don't delete)
whe_qc <- flagISNV(whe, filt)
# Check the audit log
qcLog(whe_qc)
#> DataFrame with 4 rows and 6 columns
#> step parameter value n_flagged n_passed
#> <character> <character> <character> <integer> <integer>
#> 1 depth_filter minDepth 200 1 39
#> 2 min_freq_filter minFreq 0.03 0 39
#> 3 max_freq_filter maxFreq 0.5 0 39
#> 4 alt_reads_filter minAltReads 10 0 39
#> timestamp
#> <character>
#> 1 2026-09-18T19:51:29
#> 2 2026-09-18T19:51:29
#> 3 2026-09-18T19:51:29
#> 4 2026-09-18T19:51:29# Per-sample QC summary
qcSummary(whe_qc)
#> DataFrame with 2 rows and 7 columns
#> sample_id total_sites passed_sites flagged_sites pass_rate median_depth
#> <character> <integer> <integer> <integer> <numeric> <numeric>
#> 1 donor 15 14 1 0.933333 1600
#> 2 recipient 10 10 0 1.000000 1950
#> median_freq
#> <numeric>
#> 1 0.18
#> 2 0.25# Extract only QC-passed variants
clean <- passedISNV(whe_qc)
cat("Before:", nrow(whe_qc), "sites\n")
#> Before: 20 sites
cat("After passedISNV:", nrow(clean), "sites\n")
#> After passedISNV: 20 sitesThe two tables count different things. qcLog() counts
cells of the site-by-sample matrix, so its 40 cells include those where
a sample has no call at a site; a filter cannot fail a call that is
absent, so those cells stay passed. qcSummary() counts only
the calls each sample made. passedISNV() keeps a site if
any sample passed it: the one flagged call, at position 812 in the
donor, is absent from the recipient, so the site is kept and the row
count does not change. The pipe example above, which also masks primer
sites, is where rows are removed.
If your data includes technical replicates,
flagReplicateDiscordance() identifies sites where
replicates disagree:
rep1 <- system.file("extdata", "test_donor_rep1.vcf",
package = "WithinHostExperiment")
rep2 <- system.file("extdata", "test_donor_rep2.vcf",
package = "WithinHostExperiment")
whe_rep <- readWithinHost(
c(rep1, rep2),
colData = S4Vectors::DataFrame(
sample_id = c("rep1", "rep2"),
replicate_group = c("donor_reps", "donor_reps"),
replicate_id = c("1", "2")
),
caller = "ivar"
)
whe_rep <- flagReplicateDiscordance(whe_rep, freqTolerance = 0.05)
qcLog(whe_rep)
#> DataFrame with 2 rows and 6 columns
#> step parameter value n_flagged n_passed
#> <character> <character> <character> <integer> <integer>
#> 1 replicate_discordance freqTolerance 0.05 6 26
#> 2 replicate_discordance requireBothDetected TRUE 0 26
#> timestamp
#> <character>
#> 1 2026-09-18T19:51:29
#> 2 2026-09-18T19:51:29For amplicon-based sequencing, variants in primer binding regions are unreliable and should be flagged:
library(GenomicRanges)
primers <- GRanges("test_segment", IRanges::IRanges(
start = c(40, 880), end = c(65, 950)))
whe_primer <- flagPrimerSites(whe_qc, primers)
qcLog(whe_primer)
#> DataFrame with 5 rows and 6 columns
#> step parameter value n_flagged n_passed
#> <character> <character> <character> <integer> <integer>
#> 1 depth_filter minDepth 200 1 39
#> 2 min_freq_filter minFreq 0.03 0 39
#> 3 max_freq_filter maxFreq 0.5 0 39
#> 4 alt_reads_filter minAltReads 10 0 39
#> 5 primer_filter n_primer_regions 2 6 33
#> timestamp
#> <character>
#> 1 2026-09-18T19:51:29
#> 2 2026-09-18T19:51:29
#> 3 2026-09-18T19:51:29
#> 4 2026-09-18T19:51:29
#> 5 2026-09-18T19:51:30div <- calcDiversity(whe_qc, genomeLength = 1000L)
div
#> DataFrame with 2 rows and 7 columns
#> sample_id richness shannon pi watterson simpson chao1
#> <character> <integer> <numeric> <numeric> <numeric> <numeric> <numeric>
#> 1 donor 14 6.83667 0.00446839 0.00175997 4.4656 14
#> 2 recipient 10 4.92373 0.00324286 0.00122661 3.2412 10For a lightweight MLE estimate using the beta-binomial method:
nb <- quickBottleneck(whe_qc, pairId = "pair_1", maxNb = 50L)
cat("Nb MLE:", nb$Nb, "\n")
#> Nb MLE: 3
cat("95% CI: [", nb$ci[1], ",", nb$ci[2], "]\n")
#> 95% CI: [ 1 , 7 ]asViralBottleneckInput() writes the donor and recipient
frequencies of a pair into one frame, which is the input a beta-binomial
estimator needs:
WithinHostExperiment objects can be converted to
standard Bioconductor and base R types for use with other packages.
df <- as(whe_qc, "data.frame")
head(df)
#> chrom position ref alt sample_id altFreq totalDepth refCount altCount
#> 1 test_segment 45 C T donor 0.35 1500 975 525
#> 2 test_segment 112 A T donor 0.08 800 736 64
#> 3 test_segment 198 A G donor 0.22 2200 1716 484
#> 4 test_segment 267 C A donor 0.45 3100 1705 1395
#> 5 test_segment 334 G T donor 0.12 1200 1056 144
#> 6 test_segment 401 C T donor 0.06 500 470 30
#> qcPass
#> 1 TRUE
#> 2 TRUE
#> 3 TRUE
#> 4 TRUE
#> 5 TRUE
#> 6 TRUEFor integration with VariantAnnotation workflows:
vr <- as(whe_qc, "VRanges")
vr
#> VRanges object with 25 ranges and 0 metadata columns:
#> seqnames ranges strand ref alt
#> <Rle> <IRanges> <Rle> <character> <characterOrRle>
#> [1] test_segment 45 * C T
#> [2] test_segment 112 * A T
#> [3] test_segment 198 * A G
#> [4] test_segment 267 * C A
#> [5] test_segment 334 * G T
#> ... ... ... ... ... ...
#> [21] test_segment 478 * G T
#> [22] test_segment 523 * A G
#> [23] test_segment 634 * C G
#> [24] test_segment 650 * T A
#> [25] test_segment 889 * A C
#> totalDepth refDepth altDepth sampleNames
#> <integerOrRle> <integerOrRle> <integerOrRle> <factorOrRle>
#> [1] 1500 975 525 donor
#> [2] 800 736 64 donor
#> [3] 2200 1716 484 donor
#> [4] 3100 1705 1395 donor
#> [5] 1200 1056 144 donor
#> ... ... ... ... ...
#> [21] 900 810 90 recipient
#> [22] 2100 1365 735 recipient
#> [23] 1900 1121 779 recipient
#> [24] 400 380 20 recipient
#> [25] 3200 2240 960 recipient
#> softFilterMatrix
#> <matrix>
#> [1]
#> [2]
#> [3]
#> [4]
#> [5]
#> ... ...
#> [21]
#> [22]
#> [23]
#> [24]
#> [25]
#> -------
#> seqinfo: 1 sequence from an unspecified genome; no seqlengths
#> hardFilters: NULLThe frame shown above under “Quick bottleneck estimation” carries the
positions and the two frequencies, which is what a beta-binomial
estimator takes. ViralBottleneck itself builds its objects
from per-base read counts through
CreateTransmissionObject(), so prepare those from the
altCount and refCount assays rather than from
this frame.
Allele frequency spectrum of the QC-flagged example pair.
Donor against recipient allele frequency at each site.
Four-panel QC dashboard for the example object.
calcDiversity() and quickBottleneck() give
the figures a report on a transmission pair usually needs:
div <- calcDiversity(whe_qc, genomeLength = 1000L)
cat("Shannon entropy:", round(div$shannon[1], 2), "\n")
#> Shannon entropy: 6.84
cat("Nucleotide diversity (pi):", format(div$pi[1], digits = 3), "\n")
#> Nucleotide diversity (pi): 0.00447
cat("Richness:", div$richness[1], "iSNV sites\n")
#> Richness: 14 iSNV sites
nb <- quickBottleneck(whe_qc, pairId = "pair_1", maxNb = 50L)
cat("Bottleneck Nb:", nb$Nb, "(95% CI:", nb$ci[1], "-", nb$ci[2], ")\n")
#> Bottleneck Nb: 3 (95% CI: 1 - 7 )The example files describe a synthetic 1,000-base segment, so these
numbers show the form of the output and are not evidence that the
estimators are accurate. validateDiversity(), used below,
compares an estimate with the package’s own plausibility range for a
pathogen: a sanity check, not a validation.
This section demonstrates the complete workflow on real SARS-CoV-2
iSNV data from Bendall et al. (2023) Nature Communications
14:272. We analyse household HH6 from Bendall et al. (2023).
The study’s pair table lists this household in both directions, as
HH6_A and HH6_B, so it does not establish who
infected whom; the example follows HH6_A and treats MHM1143
as the donor for illustration.
donor_file <- system.file("extdata", "MHM1143_rep1.tsv",
package = "WithinHostExperiment")
recip_file <- system.file("extdata", "MHM1184_rep1.tsv",
package = "WithinHostExperiment")
whe_sars2 <- readWithinHostTable(
c(donor_file, recip_file),
format = "ivar",
colData = S4Vectors::DataFrame(
sample_id = c("MHM1143", "MHM1184"),
role = c("donor", "recipient"),
pair_id = c("HH6", "HH6")
)
)
whe_sars2
#> WithinHostExperiment with 4 variant sites and 2 samples
#> assays(5): altFreq, totalDepth, refCount, altCount, qcPass
#> qcLog: 0 filtering step(s)
#> transmission pairs: 1whe_sars2 <- flagISNV(whe_sars2,
ISNVFilter(minDepth = 100L, minFreq = 0.03))
qcSummary(whe_sars2)
#> DataFrame with 2 rows and 7 columns
#> sample_id total_sites passed_sites flagged_sites pass_rate median_depth
#> <character> <integer> <integer> <integer> <numeric> <numeric>
#> 1 MHM1143 2 2 0 1.0 1714
#> 2 MHM1184 2 1 1 0.5 2667
#> median_freq
#> <numeric>
#> 1 0.0470586
#> 2 0.0328049
div_sars2 <- calcDiversity(whe_sars2, genomeLength = 29903L)
div_sars2
#> DataFrame with 2 rows and 7 columns
#> sample_id richness shannon pi watterson simpson chao1
#> <character> <integer> <numeric> <numeric> <numeric> <numeric> <numeric>
#> 1 MHM1143 2 0.378533 5.99618e-06 8.33587e-06 0.1791992 2
#> 2 MHM1184 1 0.156079 2.34206e-06 3.95021e-06 0.0700084 1To show that the workflow is not specific to SARS-CoV-2, we run it on
small synthetic example files for three further pathogens and check the
diversity estimates against the plausibility ranges of
validateDiversity(). The files are illustrative fixtures,
not measurements, and the ranges are the package’s own heuristics, so
these sections demonstrate the interface rather than validate the
estimates.
flu_file <- system.file("extdata", "test_influenza_donor.tsv",
package = "WithinHostExperiment")
whe_flu <- readWithinHostTable(flu_file, format = "ivar",
colData = S4Vectors::DataFrame(sample_id = "flu_donor"))
whe_flu <- flagISNV(whe_flu, adaptiveFilter("illumina"))
div_flu <- calcDiversity(whe_flu, genomeLength = 1701L,
indices = c("pi", "shannon"))
cat("Influenza pi:", format(div_flu$pi[1], digits = 3), "\n")
#> Influenza pi: 0.00255
cat("Influenza Shannon:", round(div_flu$shannon[1], 2), "\n")
#> Influenza Shannon: 6.47
validateDiversity(div_flu, pathogen = "influenza")
#> DataFrame with 2 rows and 5 columns
#> metric observed expected_min expected_max in_range
#> <character> <numeric> <numeric> <numeric> <logical>
#> 1 shannon 6.46662479 0e+00 2e+01 TRUE
#> 2 pi 0.00255148 1e-05 5e-03 TRUEhiv_file <- system.file("extdata", "test_hiv_sample.tsv",
package = "WithinHostExperiment")
whe_hiv <- readWithinHostTable(hiv_file, format = "ivar",
colData = S4Vectors::DataFrame(sample_id = "hiv_sample"))
whe_hiv <- flagISNV(whe_hiv, adaptiveFilter("illumina"))
div_hiv <- calcDiversity(whe_hiv, genomeLength = 1000L,
indices = c("pi", "shannon"))
cat("HIV pi:", format(div_hiv$pi[1], digits = 3), "\n")
#> HIV pi: 0.00663
cat("HIV Shannon:", round(div_hiv$shannon[1], 2), "\n")
#> HIV Shannon: 10.01
validateDiversity(div_hiv, pathogen = "hiv")
#> DataFrame with 2 rows and 5 columns
#> metric observed expected_min expected_max in_range
#> <character> <numeric> <numeric> <numeric> <logical>
#> 1 shannon 10.01379070 0e+00 30.00 TRUE
#> 2 pi 0.00663095 1e-04 0.05 TRUEtb_file <- system.file("extdata", "test_tb_sample.tsv",
package = "WithinHostExperiment")
whe_tb <- readWithinHostTable(tb_file, format = "ivar",
colData = S4Vectors::DataFrame(sample_id = "tb_sample"))
whe_tb <- flagISNV(whe_tb, adaptiveFilter("illumina"))
div_tb <- calcDiversity(whe_tb, genomeLength = 4411532L,
indices = c("pi", "shannon"))
cat("TB pi:", format(div_tb$pi[1], digits = 3), "\n")
#> TB pi: 6.4e-08
cat("TB Shannon:", round(div_tb$shannon[1], 2), "\n")
#> TB Shannon: 0.58
validateDiversity(div_tb, pathogen = "tb")
#> DataFrame with 2 rows and 5 columns
#> metric observed expected_min expected_max in_range
#> <character> <numeric> <numeric> <numeric> <logical>
#> 1 shannon 5.81456e-01 0e+00 5e+00 TRUE
#> 2 pi 6.40488e-08 1e-09 1e-04 TRUEnt_flu <- calcNeutralityTests(whe_flu, genomeLength = 1701L)
nt_hiv <- calcNeutralityTests(whe_hiv, genomeLength = 1000L)
nt_tb <- calcNeutralityTests(whe_tb, genomeLength = 4411532L)
results <- data.frame(
pathogen = c("Influenza", "HIV-1", "M. tuberculosis"),
n_sites = c(nrow(whe_flu), nrow(whe_hiv), nrow(whe_tb)),
tajimaD = c(nt_flu$tajimaD, nt_hiv$tajimaD, nt_tb$tajimaD),
fusFs = c(nt_flu$fusFs, nt_hiv$fusFs, nt_tb$fusFs)
)
results
#> pathogen n_sites tajimaD fusFs
#> 1 Influenza 12 2.3654771 0.9553573
#> 2 HIV-1 20 2.1215611 -0.7177965
#> 3 M. tuberculosis 3 -0.9116534 -1.7188556These fixtures are synthetic, and the tuberculosis one has only three sites, so the signs above carry no biological meaning. In real within-host data a negative Tajima’s D or Fu’s Fs is often read as purifying selection or expansion after a bottleneck, and a positive value as balancing selection or population structure; either reading needs far more sites than these examples hold.
This section demonstrates additional analytical modules available in
WithinHostExperiment. Each subsection provides a minimal
working example.
Tajima’s D and Fu’s Fs detect departures from neutral evolution. The
calcNeutralityTests() method caps the effective sample size
at n = 100 to avoid deep-sequencing bias (see
?tajimaD).
Annotate variant sites with gene names from a GFF3 file. This
automates the positional overlap needed for
dndsWithinHost():
gff_file <- tempfile(fileext = ".gff3")
writeLines(c(
"##gff-version 3",
"test_segment\t.\tgene\t1\t500\t.\t+\t.\tName=geneA",
"test_segment\t.\tgene\t501\t1000\t.\t+\t.\tName=geneB"
), gff_file)
whe_ann <- annotateFromGFF(whe_qc, gff_file)
table(S4Vectors::mcols(
SummarizedExperiment::rowRanges(whe_ann))$GFF_FEATURE)
#>
#> geneA geneB
#> 11 9For codon-aware annotation (amino acid changes), use
annotateCodonChange() with a GFF3 and reference FASTA:
ref_fa <- system.file("extdata", "test_ref.fa",
package = "WithinHostExperiment")
if (file.exists(ref_fa)) {
gff_cds <- tempfile(fileext = ".gff3")
writeLines(c("##gff-version 3",
"test_segment\t.\tCDS\t1\t999\t.\t+\t0\tgene=testGene"),
gff_cds)
whe_codon <- annotateCodonChange(whe_qc, gff_cds, ref_fa)
mc <- S4Vectors::mcols(
SummarizedExperiment::rowRanges(whe_codon))
table(mc$AA_CLASS)
}
#>
#> Nonsense Nonsynonymous Synonymous
#> 5 12 3For complex genomes with overlapping reading frames or non-standard codon usage, consider SnpEff or ANNOVAR upstream.
dndsWithinHost() counts synonymous and nonsynonymous
iSNVs, divides each by the synonymous and nonsynonymous sites of the
coding sequences (Nei and Gojobori 1986) and applies the Jukes-Cantor
correction. It uses the codon annotation from
annotateCodonChange() together with the same GFF3 and
reference FASTA:
if (file.exists(ref_fa)) {
dnds <- dndsWithinHost(whe_codon, gff = gff_cds, refFasta = ref_fa)
dnds[, c("sample_id", "nS", "nN", "S_sites", "N_sites", "dNdS")]
}
#> DataFrame with 2 rows and 6 columns
#> sample_id nS nN S_sites N_sites dNdS
#> <character> <integer> <integer> <numeric> <numeric> <numeric>
#> 1 donor 3 11 219.333 725.667 1.10937
#> 2 recipient 1 9 219.333 725.667 2.73465For longitudinal data, estimate the selection coefficient from the logit-transformed frequency change:
When data from multiple variant callers are available, create a consensus call set:
gr1 <- GenomicRanges::GRanges("seg",
IRanges::IRanges(c(100, 200, 300), width = 1))
S4Vectors::mcols(gr1)$ref <- c("A", "C", "G")
S4Vectors::mcols(gr1)$alt <- c("T", "G", "A")
whe1 <- WithinHostExperiment(
assays = list(altFreq = matrix(c(0.10, 0.20, 0.30), ncol = 1)),
rowRanges = gr1,
colData = S4Vectors::DataFrame(sample_id = "S1"))
gr2 <- GenomicRanges::GRanges("seg",
IRanges::IRanges(c(100, 200, 400), width = 1))
S4Vectors::mcols(gr2)$ref <- c("A", "C", "T")
S4Vectors::mcols(gr2)$alt <- c("T", "G", "C")
whe2 <- WithinHostExperiment(
assays = list(altFreq = matrix(c(0.12, 0.18, 0.15), ncol = 1)),
rowRanges = gr2,
colData = S4Vectors::DataFrame(sample_id = "S1"))
cons <- consensusISNV(list(ivar = whe1, lofreq = whe2))
cons
#> WithinHostExperiment with 2 variant sites and 1 samples
#> assays(2): altFreq, qcPass
#> qcLog: 0 filtering step(s)
#> transmission pairs: 0
## Pairwise caller concordance
calcCallerConcordance(list(ivar = whe1, lofreq = whe2))
#> DataFrame with 1 row and 6 columns
#> caller1 caller2 n_shared n_only1 n_only2 jaccard
#> <character> <character> <integer> <integer> <integer> <numeric>
#> 1 ivar lofreq 2 1 1 0.5exactBottleneck() offers three estimators beyond the
approximate quickBottleneck(): "exact_bb",
"wright_fisher" and "presence_absence". The
presence-absence estimator is shown here.
Track variant frequencies across time points within a host:
gr_l <- GenomicRanges::GRanges("seg1",
IRanges::IRanges(c(100, 200), width = 1))
S4Vectors::mcols(gr_l)$ref <- c("A", "C")
S4Vectors::mcols(gr_l)$alt <- c("T", "G")
freq_l <- matrix(c(0.02, 0.10, 0.12, 0.15),
nrow = 2, ncol = 2)
whe_l <- WithinHostExperiment(
assays = list(altFreq = freq_l),
rowRanges = gr_l,
colData = S4Vectors::DataFrame(
sample_id = c("t1", "t2"),
host_id = c("H1", "H1"),
timepoint = c(1, 2)))
## Track frequencies
trackFrequency(whe_l)
#> DataFrame with 4 rows and 5 columns
#> host_id variant_key timepoint frequency qc_passed
#> <character> <character> <numeric> <numeric> <logical>
#> 1 H1 seg1:100:T 1 0.02 TRUE
#> 2 H1 seg1:200:G 1 0.10 TRUE
#> 3 H1 seg1:100:T 2 0.12 TRUE
#> 4 H1 seg1:200:G 2 0.15 TRUE
## Detect emerging variants
detectEmergingVariants(whe_l)
#> DataFrame with 1 row and 7 columns
#> host_id variant_key time_from time_to freq_from freq_to delta_freq
#> <character> <character> <numeric> <numeric> <numeric> <numeric> <numeric>
#> 1 H1 seg1:100:T 1 2 0.02 0.12 0.1The SCI score normalises strand bias by the global strand coverage ratio, which suits amplicon data with unequal strand coverage:
Pre-configured filter presets for different sequencing platforms:
adaptiveFilter("illumina")
#> ISNVFilter object
#> minDepth: 100
#> freq range: [0.03, 0.5]
#> minAltReads: 10
#> maxStrandBias: 10
#> minBaseQual: 20
#> minMapQual: 20
#> replicateConc: FALSE
adaptiveFilter("ont")
#> ISNVFilter object
#> minDepth: 60
#> freq range: [0.1, 0.5]
#> minAltReads: 10
#> maxStrandBias: Inf
#> minBaseQual: 20
#> minMapQual: 20
#> replicateConc: FALSE
adaptiveFilter("pacbio_hifi")
#> ISNVFilter object
#> minDepth: 50
#> freq range: [0.05, 0.5]
#> minAltReads: 10
#> maxStrandBias: 10
#> minBaseQual: 20
#> minMapQual: 20
#> replicateConc: FALSEFit linear models to diversity data and correct for multiple testing:
div_df <- data.frame(
shannon = c(1.2, 1.5, 0.8, 1.1, 1.9, 0.7),
timepoint = c(1, 2, 3, 1, 2, 3),
group = c("A", "A", "A", "B", "B", "B"))
res <- fitDiversityModel(div_df, shannon ~ timepoint,
group_col = "group")
res$summary_df
#> term estimate std_error t_value p_value
#> 1 (Intercept) 1.56666667 0.6085806 2.5742960 0.08218575
#> 2 timepoint -0.20000000 0.2635231 -0.7589466 0.50306833
#> 3 groupB 0.06666667 0.4303315 0.1549193 0.88672102
## Multiple testing correction
raw_p <- c(0.001, 0.04, 0.03, 0.5)
multitestCorrection(raw_p)
#> [1] 0.00400000 0.05333333 0.05333333 0.50000000Cross-check observed diversity against the package’s plausibility range for a given pathogen:
div_check <- S4Vectors::DataFrame(pi = 1e-4, shannon = 5.0)
validateDiversity(div_check, pathogen = "sars_cov_2")
#> DataFrame with 2 rows and 5 columns
#> metric observed expected_min expected_max in_range
#> <character> <numeric> <numeric> <numeric> <logical>
#> 1 pi 1e-04 1e-06 0.001 TRUE
#> 2 shannon 5e+00 0e+00 15.000 TRUEThis package operates at the level of individual single nucleotide variants (iSNVs). It does not perform:
Haplotype phasing or quasispecies
reconstruction, which require read-level (BAM) analysis outside
the scope of RangedSummarizedExperiment. For
haplotype-aware analysis, consider CliqueSNV (Knyazev et al. 2021
Nucleic Acids Res 49:e102), ShoRAH (Zagordi et al. 2011 BMC
Bioinformatics 12:119), or similar tools. Their output can be
imported into WithinHostExperiment for downstream diversity
and QC analysis.
Co-infection vs diversity distinction, which requires phylogenetic analysis of full haplotypes. The multi-caller consensus and replicate-concordance modules reduce artefacts but do not resolve this.
Stochastic selection coefficient estimation. The
estimateSelectionCoefficient() function uses a
deterministic logit-frequency model. For drift-aware inference, consider
dedicated Wright-Fisher or diffusion-based frameworks.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] GenomicRanges_1.65.4 Seqinfo_1.3.2
#> [3] IRanges_2.47.5 S4Vectors_0.51.10
#> [5] BiocGenerics_0.59.12 generics_0.1.4
#> [7] WithinHostExperiment_0.99.0 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] tidyselect_1.2.1 dplyr_1.2.1
#> [3] farver_2.1.2 blob_1.3.0
#> [5] Biostrings_2.81.9 S7_0.2.2
#> [7] bitops_1.1-0 fastmap_1.2.0
#> [9] RCurl_1.98-1.20 VariantAnnotation_1.59.4
#> [11] GenomicAlignments_1.49.2 XML_3.99-0.24
#> [13] digest_0.6.39 lifecycle_1.0.5
#> [15] KEGGREST_1.53.6 RSQLite_3.53.3
#> [17] magrittr_2.0.5 compiler_4.6.1
#> [19] rlang_1.3.0 sass_0.4.10
#> [21] tools_4.6.1 yaml_2.3.12
#> [23] rtracklayer_1.73.0 knitr_1.52
#> [25] S4Arrays_1.13.0 labeling_0.4.3
#> [27] bit_4.6.0 curl_8.0.0
#> [29] DelayedArray_0.39.6 RColorBrewer_1.1-3
#> [31] abind_1.4-8 BiocParallel_1.47.0
#> [33] withr_3.0.3 sys_3.4.3
#> [35] grid_4.6.1 ggplot2_4.0.3
#> [37] scales_1.4.0 SummarizedExperiment_1.43.0
#> [39] cli_3.6.6 crayon_1.5.3
#> [41] otel_0.2.0 httr_1.4.9
#> [43] rjson_0.2.23 BiocBaseUtils_1.15.1
#> [45] DBI_1.3.0 cachem_1.1.0
#> [47] parallel_4.6.1 AnnotationDbi_1.75.2
#> [49] XVector_0.53.0 restfulr_0.0.17
#> [51] matrixStats_1.5.0 vctrs_0.7.3
#> [53] Matrix_1.7-6 jsonlite_2.0.0
#> [55] patchwork_1.3.2 bit64_4.8.6
#> [57] GenomicFeatures_1.65.0 maketools_1.3.2
#> [59] jquerylib_0.1.4 glue_1.8.1
#> [61] codetools_0.2-20 gtable_0.3.6
#> [63] GenomeInfoDb_1.49.1 BiocIO_1.23.3
#> [65] UCSC.utils_1.9.0 tibble_3.3.1
#> [67] pillar_1.11.1 htmltools_0.5.9
#> [69] BSgenome_1.81.1 R6_2.6.1
#> [71] evaluate_1.0.5 lattice_0.23-1
#> [73] Biobase_2.73.2 png_0.1-9
#> [75] Rsamtools_2.29.0 cigarillo_1.3.1
#> [77] memoise_2.0.1 bslib_0.12.0
#> [79] SparseArray_1.13.2 xfun_0.61
#> [81] MatrixGenerics_1.25.0 buildtools_1.0.0
#> [83] pkgconfig_2.0.3