Skip to content

Commit 02a78e4

Browse files
committed
feat: add low-confidence site filtering
Ensures missing variants are distinguished from zero frequency values by applying depth and quality filters. Prevents low-confidence sites from biasing allele frequency analyses.
1 parent d3bae8a commit 02a78e4

6 files changed

Lines changed: 120 additions & 37 deletions

File tree

template.qmd

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ n.samples <- table %>% pull(Sample) %>% unique() %>% length()
9292
heatmap.df <- read.csv(params$heat_tab)
9393
row.names(heatmap.df) <- heatmap.df$`...1`
9494
heatmap.df[1] <- NULL
95-
cor.mat <- cor(heatmap.df, method = params$cor_method)
96-
cor.mat[is.na(cor.mat)] <- 0
95+
cor.mat <- cor(heatmap.df, method = params$cor_method, use = "pairwise.complete.obs")
9796
if (params$cor_method == "pearson") {
9897
cor.method.name <- "Pearson's"
9998
} else if (params$cor_method == "spearman") {
@@ -247,10 +246,11 @@ Labeled dots represent nucleotide variants whose allele frequency is correlated
247246

248247
Significantly correlated nucleotide variants are described in more detail in @fig-panel.
249248

250-
![Time series of relative allele frequencies. The shown positions include
251-
nucleotide variants with a significant correlation with time and sites with more
252-
than two possible states. Each subplot depicts the progression of the allele
253-
frequencies in time for a given genome position.](`r params$panel`){#fig-panel}
249+
![Time series of relative allele frequencies. Nucleotide variants with a significant
250+
correlation with time and sites with more than two possible states are included. Each
251+
subplot depicts the progression of the allele frequencies in time for a given genome
252+
position; lines connecting points indicate data from contiguous time points, while
253+
unconnected points denote intervals with missing data.](`r params$panel`){#fig-panel}
254254

255255
A `r dist.tree.algo` tree has been constructed using pairwise distances
256256
between target samples (@fig-tree), based on the allele frequencies measured from
@@ -277,8 +277,18 @@ The heatmap is interactive and allows zooming in on specific regions.
277277
```{r fig-heatmap, echo = F, message = F, warning = F, fig.align = 'center'}
278278
#| fig-cap: "Interactive heatmap with hierarchical clustering of the pairwise correlation coefficients between the time series of allele frequencies in the case study."
279279
280+
# Calculate dendrograms using data with no NAs
281+
cor.mat.clustering <- cor.mat
282+
cor.mat.clustering[is.na(cor.mat.clustering)] <- 0
283+
dist.rows <- dist(cor.mat.clustering)
284+
dend.rows <- hclust(dist.rows)
285+
dend.cols <- hclust(dist(t(cor.mat.clustering)))
286+
287+
# Display custom dendrograms over real data
280288
heatmaply_cor(
281289
cor.mat,
290+
Rowv = as.dendrogram(dend.rows),
291+
Colv = as.dendrogram(dend.cols),
282292
grid_gap = 0.25,
283293
fontsize_row = 7,
284294
fontsize_col = 7,

workflow/rules/report.smk

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ rule af_time_correlation_data:
345345
cor_method = config["COR"]["METHOD"],
346346
cor_exact = config["COR"]["EXACT"],
347347
input:
348-
variants = OUTDIR/f"{OUTPUT_NAME}.variants.tsv",
348+
variants = OUTDIR/f"{OUTPUT_NAME}.variants.all_sites.tsv",
349349
metadata = config["METADATA"],
350350
output:
351351
fmt_variants = temp(REPORT_DIR_TABLES/"variants.filled.dated.tsv"),
@@ -392,6 +392,19 @@ rule af_trajectory_panel_plot:
392392
"../scripts/report/af_trajectory_panel_plot.R"
393393

394394

395+
rule pairwise_trajectory_correlation_data:
396+
conda: "../envs/renv.yaml"
397+
input:
398+
variants = OUTDIR/f"{OUTPUT_NAME}.variants.all_sites.tsv",
399+
metadata = config["METADATA"],
400+
output:
401+
table = report(REPORT_DIR_TABLES/"pairwise_trajectory_correlation.csv"),
402+
log:
403+
LOGDIR / "pairwise_trajectory_correlation_data" / "log.txt"
404+
script:
405+
"../scripts/pairwise_trajectory_correlation_data.R"
406+
407+
395408
rule summary_table:
396409
conda: "../envs/renv.yaml"
397410
input:
@@ -423,7 +436,7 @@ rule report:
423436
temest = report(REPORT_DIR_PLOTS/"time_signal.png"),
424437
evo = report(REPORT_DIR_PLOTS/"dn_and_ds.png"),
425438
omega_plot = report(REPORT_DIR_PLOTS/"dnds.png"),
426-
heat_table = report(OUTDIR/"vaf"/"pairwise_trajectory_correlation.csv"),
439+
heat_table = report(REPORT_DIR_TABLES/"pairwise_trajectory_correlation.csv"),
427440
freyja_ts = OUTDIR/"demixing"/"freyja_data"/"last_barcode_update.txt",
428441
value = REPORT_DIR_TABLES/"diversity.json",
429442
stats_lm = REPORT_DIR_TABLES/"time_signal.json",

workflow/rules/vaf.smk

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,8 @@ rule bcftools_mpileup_all_sites:
233233
bam = get_input_bam,
234234
reference = OUTDIR/"vaf"/"{sample}.reference.fasta",
235235
output:
236-
mpileup = OUTDIR / "bcftools_mpileup_all_sites" / "{sample}.mpileup.vcf",
237-
query = OUTDIR / "bcftools_mpileup_all_sites" / "{sample}.query.tsv",
236+
mpileup = temp(OUTDIR / "all_sites" / "{sample}.mpileup.vcf"),
237+
query = temp(OUTDIR / "all_sites" / "{sample}.query.tsv"),
238238
log:
239239
mpileup = LOGDIR / "bcftools_mpileup_all_sites" / "{sample}.mpileup.txt",
240240
query = LOGDIR / "bcftools_mpileup_all_sites" / "{sample}.query.txt",
@@ -251,33 +251,41 @@ rule filter_mpileup_all_sites:
251251
min_total_ADF = 0,
252252
min_total_ADR = 0,
253253
input:
254-
OUTDIR / "bcftools_mpileup_all_sites" / "{sample}.query.tsv",
254+
OUTDIR / "all_sites" / "{sample}.query.tsv",
255255
output:
256-
OUTDIR / "bcftools_mpileup_all_sites" / "{sample}.filtered.tsv",
256+
temp(OUTDIR / "all_sites" / "{sample}.filtered_sites.tsv"),
257257
log:
258258
LOGDIR / "filter_mpileup_all_sites" / "{sample}.txt"
259259
run:
260260
import pandas as pd
261261
df = pd.read_csv(input[0], sep="\t")
262-
df["ref_AD"] = df.AD.str.split(",").apply(lambda values: int(values[0]))
263-
df["total_AD"] = df.AD.str.split(",").apply(lambda values: sum(int(n) for n in values))
264-
df["total_ADF"] = df.ADF.str.split(",").apply(lambda values: sum(int(n) for n in values))
265-
df["total_ADR"] = df.ADR.str.split(",").apply(lambda values: sum(int(n) for n in values))
262+
df["SAMPLE"] = wildcards.sample
263+
df["REF_AD"] = df.AD.str.split(",").apply(lambda values: int(values[0]))
264+
df["TOTAL_AD"] = df.AD.str.split(",").apply(lambda values: sum(int(n) for n in values))
265+
df["TOTAL_ADF"] = df.ADF.str.split(",").apply(lambda values: sum(int(n) for n in values))
266+
df["TOTAL_ADR"] = df.ADR.str.split(",").apply(lambda values: sum(int(n) for n in values))
266267
df[
267-
(df.total_AD >= params.min_total_AD) &
268-
(df.total_ADF >= params.min_total_ADF) &
269-
(df.total_ADR >= params.min_total_ADR)
268+
(df.TOTAL_AD >= params.min_total_AD) &
269+
(df.TOTAL_ADF >= params.min_total_ADF) &
270+
(df.TOTAL_ADR >= params.min_total_ADR)
270271
].to_csv(output[0], sep="\t", index=False)
271272

272273

273-
rule pairwise_trajectory_correlation:
274+
use rule concat_vcf_fields as merge_filtered_mpileup_all_sites with:
275+
input:
276+
expand(OUTDIR / "all_sites" / "{sample}.filtered_sites.tsv", sample=iter_samples()),
277+
output:
278+
OUTDIR / f"{OUTPUT_NAME}.filtered_sites.tsv",
279+
280+
281+
rule fill_all_sites:
274282
conda: "../envs/renv.yaml"
275283
input:
276-
variants = OUTDIR/f"{OUTPUT_NAME}.variants.tsv",
277-
metadata = config["METADATA"],
284+
variants = OUTDIR/f"{OUTPUT_NAME}.variants.tsv",
285+
sites = OUTDIR / f"{OUTPUT_NAME}.filtered_sites.tsv",
278286
output:
279-
table = report(OUTDIR/"vaf"/"pairwise_trajectory_correlation.csv"),
287+
variants = OUTDIR/f"{OUTPUT_NAME}.variants.all_sites.tsv",
280288
log:
281-
LOGDIR / "pairwise_trajectory_correlation" / "log.txt"
289+
LOGDIR / "fill_all_sites" / "log.txt"
282290
script:
283-
"../scripts/pairwise_trajectory_correlation.R"
291+
"../scripts/fill_all_sites.R"

workflow/scripts/fill_all_sites.R

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env Rscript
2+
3+
# Write stdout and stderr to log file
4+
log <- file(snakemake@log[[1]], open = "wt")
5+
sink(log, type = "message")
6+
sink(log, type = "output")
7+
8+
library(dplyr)
9+
library(readr)
10+
library(tidyr)
11+
library(logger)
12+
13+
log_threshold(INFO)
14+
15+
log_info("Reading variants")
16+
variants <- read_tsv(snakemake@input[["variants"]])
17+
18+
# Create a mapping of variant names to their genomic position
19+
variant_coords <- variants %>%
20+
select(VARIANT_NAME, REGION, POS) %>%
21+
distinct()
22+
23+
log_info("Reading filtered sites")
24+
sites <- read_tsv(snakemake@input[["sites"]]) %>%
25+
select(SAMPLE, POS) %>% # TODO: consider region/chrom
26+
distinct() %>%
27+
mutate(FILTER_PASS = TRUE)
28+
29+
log_info("Processing variants")
30+
all_variants <- variants %>%
31+
# Select minimal columns
32+
select(VARIANT_NAME, REGION, SAMPLE, ALT_FREQ) %>%
33+
# Handle duplicates
34+
group_by(SAMPLE, VARIANT_NAME, REGION) %>%
35+
summarise(ALT_FREQ = sum(ALT_FREQ, na.rm = TRUE), .groups = "drop") %>%
36+
# Complete with NA
37+
complete(SAMPLE, VARIANT_NAME, REGION) %>%
38+
# Assign genomic positions for all combinations
39+
left_join(variant_coords, by = c("REGION", "VARIANT_NAME")) %>%
40+
# Merge filtered sites
41+
# TODO: consider region/chrom
42+
left_join(sites, by = c("SAMPLE", "POS")) %>%
43+
replace_na(list(FILTER_PASS = FALSE)) %>%
44+
# Fill missing frequencies conditionally
45+
mutate(
46+
ALT_FREQ = case_when(
47+
!is.na(ALT_FREQ) ~ ALT_FREQ, # variant was called, keep the frequency
48+
FILTER_PASS ~ 0, # missing but site is covered: 0 (reference)
49+
TRUE ~ NA # missing and not covered: NA (unknown)
50+
)
51+
)
52+
53+
log_info("Saving table")
54+
write_tsv(all_variants, snakemake@output[["variants"]])

workflow/scripts/pairwise_trajectory_correlation.R renamed to workflow/scripts/pairwise_trajectory_correlation_data.R

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ library(logger)
1414

1515
log_threshold(INFO)
1616

17+
log_info("Reading variants")
1718
variants <- read_tsv(snakemake@input[["variants"]])
1819

1920
# Obtain sample names ordered by CollectionDate
@@ -22,20 +23,17 @@ date_order <- read_csv(snakemake@input[["metadata"]]) %>%
2223
pull(ID) %>%
2324
unique()
2425

25-
# Create SNP variable and select useful variables
26-
variants <- variants %>% select(VARIANT_NAME, SAMPLE, ALT_FREQ)
27-
28-
variants <- variants %>%
26+
all_variants_wider <- variants %>%
27+
select(SAMPLE, VARIANT_NAME, ALT_FREQ) %>%
2928
pivot_wider(
3029
names_from = VARIANT_NAME,
31-
values_from = ALT_FREQ,
32-
values_fill = 0,
33-
values_fn = sum
30+
values_from = ALT_FREQ
3431
) %>%
32+
# Apply chronological ordering
3533
arrange(factor(SAMPLE, levels = date_order)) %>%
36-
# Removes "|"-separated annotations, keeping the first one + ellipsis (clarifies heatmap)
34+
# Removes "|"-separated annotations, keeping the first one + ellipsis
3735
rename_with(~ str_replace(., "^([^|]+)\\|.*$", "\\1(...)"), -SAMPLE) %>%
3836
column_to_rownames(var = "SAMPLE")
3937

40-
log_info("Saving table to create heatmap")
41-
write.csv(variants, snakemake@output[["table"]])
38+
log_info("Saving table")
39+
write.csv(all_variants_wider, snakemake@output[["table"]])

workflow/scripts/report/af_time_correlation_data.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ variants <- read_delim(
2424
"POS"
2525
)
2626
) %>%
27-
# Fill positions without alt frequency with 0
27+
# Fill positions without alt frequency with NA
2828
complete(
2929
nesting(REGION, VARIANT_NAME, POS),
3030
SAMPLE,
31-
fill = list(ALT_FREQ = 0)
31+
fill = list(ALT_FREQ = NA)
3232
)
3333

3434
log_info("Reading metadata")

0 commit comments

Comments
 (0)