Skip to content

Commit c9f59d2

Browse files
committed
run some abundance metrics without FP and FN
1 parent c7eb46f commit c9f59d2

3 files changed

Lines changed: 63 additions & 46 deletions

File tree

bin/compare_performance.r

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ mismatch_threshold <- as.numeric(args[6])
1414

1515
# function to produce statistics
1616
get_stats <- function(i_exp,i_obs,df,sample) {
17-
1817
# if none are expected, skip!
1918
if( length(i_exp)==0 ) {
2019
print( paste("No expected seq in sample",sample, "- skipping") ); next
2120
} else {
2221
print( paste(length(i_exp),"expected seq in sample",sample) )
2322
}
24-
2523
# stats
2624
TP <- intersect(i_exp, i_obs)
2725
FN <- setdiff(i_exp, i_obs)
@@ -33,7 +31,6 @@ get_stats <- function(i_exp,i_obs,df,sample) {
3331
Fone <- 0
3432
Fbeta <- 0
3533
}
36-
3734
# save
3835
types <- c(
3936
"observed",
@@ -67,32 +64,21 @@ get_stats <- function(i_exp,i_obs,df,sample) {
6764
paste(head(FN, n=100), collapse=','),
6865
paste(head(FP, n=100), collapse=',')
6966
)
67+
# prepare output
7068
ids <- rep(sample, length(types))
71-
7269
df_append <- data.frame(
7370
sample=ids,
7471
type= types,
7572
value= values,
7673
stringsAsFactors=FALSE)
7774
df <- rbind( df, df_append )
78-
7975
return(df)
8076
}
8177

82-
# function to produce abundance statistics
83-
get_stats_abundance <- function(expected_abund,observed_abund,df,sample) {
84-
85-
# if none are expected, skip!
86-
if( length(expected_abund)==0 ) {
87-
print( paste("No expected abundances in sample",sample, "- skipping") ); next
88-
} else {
89-
print( paste(length(expected_abund),"expected seq in sample",sample) )
90-
}
91-
78+
# function to produce distances based on complete abundance statistics (incl. FN & FP)
79+
get_stats_distances <- function(expected_abund,observed_abund,df,sample) {
80+
if( length(expected_abund)==0 ) { next }
9281
# stats
93-
pearson_cor <- cor.test(expected_abund, observed_abund, method = "pearson")
94-
spearman_rho <- cor.test(expected_abund, observed_abund, method = "spearman")
95-
percent_dev <- abs((observed_abund - expected_abund) / expected_abund) * 100
9682
jensen_shannon <- function(p, q) {
9783
# KL divergence function (with pseudocount to handle zeros)
9884
kl_div <- function(x, y, pseudocount = 1e-10) {
@@ -102,46 +88,65 @@ get_stats_abundance <- function(expected_abund,observed_abund,df,sample) {
10288
y <- y / sum(y) # normalize
10389
sum(x * log(x / y))
10490
}
105-
10691
# Midpoint distribution
10792
m <- (p + q) / 2
108-
10993
# Jensen-Shannon is the square root of the average of two KL divergences
11094
sqrt(0.5 * kl_div(p, m) + 0.5 * kl_div(q, m))
11195
}
96+
# save
97+
types <- c(
98+
"bray-curtis",
99+
"hellinger",
100+
"jensen-shannon"
101+
)
102+
values <- c(
103+
1 - (2 * sum(pmin(observed_abund, expected_abund))) / (sum(observed_abund) + sum(expected_abund)), # Bray-Curtis Dissimilarity
104+
sqrt(sum((sqrt(observed_abund) - sqrt(expected_abund))^2)), # Hellinger Distance
105+
jensen_shannon(observed_abund, expected_abund) # Jensen-Shannon Divergence
106+
)
107+
# prepare output
108+
ids <- rep(sample, length(types))
109+
df_append <- data.frame(
110+
sample=ids,
111+
type=types,
112+
value=values,
113+
stringsAsFactors=FALSE)
114+
df <- rbind( df, df_append )
115+
return(df)
116+
}
112117

118+
# function to produce abundance statistics based on filtered abundance statistics (excl. FN & FP)
119+
get_stats_abundance <- function(expected_abund,observed_abund,df,sample) {
120+
if( length(expected_abund)==0 ) { next }
121+
# stats
122+
pearson_cor <- cor.test(expected_abund, observed_abund, method = "pearson")
123+
spearman_rho <- cor.test(expected_abund, observed_abund, method = "spearman")
124+
percent_dev <- abs((observed_abund - expected_abund) / expected_abund) * 100
113125
# save
114126
types <- c(
115127
"pearson_cor",
116128
"spearman_rho",
117129
"deviation",
118130
"mae",
119131
"rmse",
120-
"ps",
121-
"bray-curtis",
122-
"hellinger",
123-
"jensen-shannon"
132+
"ps"
124133
)
125134
values <- c(
126135
as.list(pearson_cor$estimate)[[1]], # Pearson's Correlation (cor)
127136
as.list(spearman_rho$estimate)[[1]], # Spearman's Rank Correlation (rho)
128137
median(percent_dev, na.rm = TRUE), # Median Percent Abundance Deviation
129138
mean(abs(observed_abund - expected_abund), na.rm = TRUE), # Mean Absolute Error (MAE)
130139
sqrt(mean((observed_abund - expected_abund)^2, na.rm = TRUE)), # Root Mean Square Error (RMSE)
131-
sum(pmin(observed_abund, expected_abund)) / sum(expected_abund), # Proportional Similarity (PS)
132-
1 - (2 * sum(pmin(observed_abund, expected_abund))) / (sum(observed_abund) + sum(expected_abund)), # Bray-Curtis Dissimilarity
133-
sqrt(sum((sqrt(observed_abund) - sqrt(expected_abund))^2)), # Hellinger Distance
134-
jensen_shannon(observed_abund, expected_abund) # Jensen-Shannon Divergence
140+
sum(pmin(observed_abund, expected_abund)) / sum(expected_abund) # Proportional Similarity (PS)
135141
)
142+
# prepare output
136143
ids <- rep(sample, length(types))
137-
138144
df_append <- data.frame(
139145
sample=ids,
140-
type= types,
141-
value= values,
146+
type=types,
147+
value=values,
142148
stringsAsFactors=FALSE)
143149
df <- rbind( df, df_append )
144-
145150
return(df)
146151
}
147152

@@ -258,8 +263,12 @@ for (sample in SAMPLES) {
258263
obsIDs <- data$ID[!is.na(data$query)]
259264
df <- get_stats( expIDs, obsIDs, df, sample)
260265

261-
# calculate abundance stats, this includes also non-matching sequences
262-
df <- get_stats_abundance( data$expected_abund, data$observed_abund, df, sample)
266+
# Calculate distance metrics, this *includes* non-matching sequences (FP & FN)
267+
df <- get_stats_distances( data$expected_abund, data$observed_abund, df, sample)
268+
269+
# Calculate relationship and abundance error, this *excludes* non-matching sequences (FP & FN)
270+
data_matches <- data[data$observed_abund >0 & data$expected_abund >0,] # only on matched observed to expected
271+
df <- get_stats_abundance( data_matches$expected_abund, data_matches$observed_abund, df, sample)
263272

264273
# (4) PLOTS - pairwise comparisons
265274

@@ -282,7 +291,6 @@ for (sample in SAMPLES) {
282291
invisible(dev.off())
283292

284293
# Scatter plot: Observed vs. Expected Abundance (log-log)
285-
data_matches <- data[data$observed_abund >0 & data$expected_abund >0,] # only on matched observed to expected
286294
outfile <- paste0(sample,"_scatter_loglog")
287295
print(paste("write",outfile))
288296
svg(paste0(outfile,".svg"), width = 8, height = 6)

docs/output.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ When expected sequences are supplied, the following files will be produced:
681681

682682
When additionally expected abundances are available, additional performance metrics will be generated.
683683

684-
(1) Metrics base on presence/absence:
684+
#### Metrics base on presence/absence:
685685

686686
- `observed`: Number of observed sequences
687687
- `expected`: Number of expected sequences
@@ -698,24 +698,31 @@ When additionally expected abundances are available, additional performance metr
698698
- `FNs_exp`: FN IDs (max 100) corresponding to expected sequences
699699
- `FPs_obs`: FP IDs (max 100) corresponding to observed sequences
700700

701-
(2) Metrics based on abundances:
701+
> [!NOTE]
702+
> The F1 score is unreliable with strongly unbalanced data.
703+
704+
> [!WARNING]
705+
> If the supplied expected sequences are not unique in the region of the alignment with the observed sequences, alignment matches are used to aggregate identical expected sequences and vice versa. In case there isnt an exact match to a set of identical expected sequences, those will not be aggregated and inflate the number of expected sequences.
706+
707+
#### Metrics based on abundances:
708+
709+
(1) based on filtered abundance tables, **excluding** non-matching sequences (FP & FN)
710+
711+
These metrics would be inflated, dominated or skewed by false positives and false negatives.
702712

703713
- `pearson_cor`: Pearson's Correlation (cor)
704714
- `spearman_rho`: Spearman's Rank Correlation (rho)
705715
- `deviation`: Median Percent Abundance Deviation
706716
- `mae`: Mean Absolute Error (MAE)
707717
- `rmse`: Root Mean Square Error (RMSE)
708718
- `ps`: Proportional Similarity (PS)
719+
720+
(2) based on complete abundance tables, **including** non-matching sequences (FP & FN)
721+
709722
- `bray-curtis`: Bray-Curtis Dissimilarity
710723
- `hellinger`: Hellinger Distance
711724
- `jensen-shannon`: Jensen-Shannon Divergence
712725

713-
> [!NOTE]
714-
> The F1 score is unreliable with strongly unbalanced data.
715-
716-
> [!WARNING]
717-
> If the supplied expected sequences are not unique in the region of the alignment with the observed sequences, alignment matches are used to aggregate identical expected sequences. In case there isnt an exact match to a set of identical expected sequences, those will not be aggregated and inflate the number of expected sequences.
718-
719726
The following additional files will be produced:
720727

721728
<details markdown="1">

docs/usage.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,10 +343,12 @@ To investigate the quality of data generation and/or data analysis, analysis out
343343
The observed sequences will be aligned globally (using `VSEASRCH --usearch_global`) to the expected sequences (`--expected_sequences`).
344344
Depending on the region to analyse (`--expected_sequences_region`) the mismatches and gaps within the alignment will be summarized with or without terminal gaps.
345345
The nucleotide differences will be evaluated for each observed sequence to its best match.
346+
346347
Expected abundances per sequence (`--expected_abundances`) enable sample specific presence/absence metrics and abundance-based comparisons.
347-
For those, observed and expected sequences will be aggregated by their analysed region: when one observed sequence matches exactely to several expected sequences, the expected sequence IDs (and abundances) will be concatenated and vice versa. That means, for example, if observed sequences are shorter than expected sequences and the analysed region is "query", each expected sequence (ID) that matches the same observed sequence will be aggregated.
348+
For those, observed and expected sequences will be aggregated by their analysed region: when one observed sequence matches exactely to several expected sequences, the expected sequence IDs will be concatenated and vice versa.
349+
That means, for example, if observed sequences are shorter than expected sequences and the analysed region is "query", each expected sequence (ID) that matches the same observed sequence will be aggregated.
348350

349-
The aggregation of obsevered and expected IDs and abundances based on perfect matches:
351+
The aggregation of obsevered and expected IDs and abundances based on perfect matches
350352

351353
| obsID | expID | exp_abund | obs_abund |
352354
| ----- | ----- | --------- | --------- |
@@ -373,7 +375,7 @@ will be transformed to:
373375
| h | | 0 | 1 |
374376
| | 4 | 1 | 0 |
375377

376-
This aggregation will not work properly when many sequences are not observed, e.g. in the above axample observed ID "a" links expected IDs "1" and "2", which would not have been aggregated if "a" would not have been observed. This would inflate expected sequences. Therefore, optimal sequence and abundance input are tailored towards the actual sequenced region and de-duplicated.
378+
This aggregation will not work properly when many sequences are not observed, e.g. in the above example observed ID "a" links expected IDs "1" and "2", which would not have been aggregated if "a" would not have been observed. This would inflate expected sequences. Therefore, optimal sequence and abundance input are tailored towards the actual sequenced region and de-duplicated.
377379

378380
### Differential abundance analysis
379381

0 commit comments

Comments
 (0)