karioCaS 0.99.16
Metagenomic classification with k-mer based tools such as Kraken2 is fast, but prone to false-positive assignments. Kraken2’s --confidence parameter (the Confidence Score, CS) controls how much k-mer evidence a read needs before it is assigned to a taxon: higher values remove spurious assignments, but also discard true signal. The usual practice of applying a single, global CS to a whole sample ignores that biological domains (Bacteria, Archaea, Eukaryota and Viruses) differ widely in database representation, genome size and k-mer uniqueness, so one threshold rarely fits all.
karioCaS (Kraken Confidence Scores for Reliable Domain-Specific Microbiota Inference and Discovery) takes the same sample classified at several CS values and evaluates how taxa and reads are lost as stringency increases, separately for each domain. From these decay curves it derives objective, domain-specific thresholds for the CS and for the minimum number of reads, and assembles a final “mosaic” profile in which each domain is filtered at its own optimal stringency.
karioCaS works upstream of most microbiome analysis. The reports produced at the different CS values are imported into a single TreeSummarizedExperiment object (taxa as rows, one column per sample and CS), so the data can be inspected or passed on with standard SummarizedExperiment accessors and with the wider ecosystem of packages built on these classes, such as mia. Existing Bioconductor packages for microbiome data start from an already chosen taxonomic profile; to our knowledge, none of them addresses how the Kraken2 confidence score used to produce that profile affects the result. karioCaS fills this gap and hands a curated, high-confidence profile to those downstream tools.
karioCaS can be installed from Bioconductor with:
if (!requireNamespace("BiocManager", quietly = TRUE)) {
install.packages("BiocManager")
}
BiocManager::install("karioCaS")
library(karioCaS)
karioCaS works on a project folder. Place one Kraken2 report per sample and CS, converted to MPA format (for example with KrakenTools’ kreport2mpa.py), inside a sub-folder named 000_mpa_original. File names must follow SAMPLENAME_CSXX.mpa, where XX encodes the CS: SAMPLE01_CS09.mpa is sample SAMPLE01 classified with --confidence 0.9 (CS00 to CS10 are read as tenths; CS40, CS100 or CS0.05 are also accepted). Include a report at CS 0 for every sample: it is the reference for the detailed per-sample retention panels.
Each analysis step writes its results to its own numbered sub-folder of the project. This vignette uses the example data shipped with the package, a single sample classified at six CS values (0, 0.2, 0.4, 0.6, 0.8 and 0.9). How these files were produced is described in inst/script/ of the package source. We copy them to a temporary folder, because the installed package folder must not be written to.
proj_dir <- file.path(tempdir(), "karioCaS_Tutorial")
dir.create(file.path(proj_dir, "000_mpa_original"), recursive = TRUE)
mock_files <- list.files(
system.file("extdata", "your_project_name", "000_mpa_original",
package = "karioCaS"
),
full.names = TRUE
)
file.copy(mock_files, file.path(proj_dir, "000_mpa_original"))
#> [1] TRUE TRUE TRUE TRUE TRUE TRUE
basename(mock_files)
#> [1] "SAMPLE01_CS00.mpa" "SAMPLE01_CS02.mpa" "SAMPLE01_CS04.mpa"
#> [4] "SAMPLE01_CS06.mpa" "SAMPLE01_CS08.mpa" "SAMPLE01_CS09.mpa"
import_karioCaS() reads every report in 000_mpa_original and assembles them into a TreeSummarizedExperiment:
SAMPLE_CSxx, with the sample name and the CS (as an integer percentage, e.g. CS09 becomes 90) stored in colData();rowData(), plus Rank, the lowest rank present in the path.No normalisation or transformation is applied: the assay holds the read counts exactly as in the reports. Note that MPA counts are cumulative: the count of a genus row includes the reads assigned to its species.
tse_object <- import_karioCaS(project_dir = proj_dir)
tse_object
#> class: TreeSummarizedExperiment
#> dim: 2030 6
#> metadata(0):
#> assays(1): counts
#> rownames(2030): d__Archaea d__Archaea|k__Methanobacteriati ...
#> d__Bacteria|p__Pseudomonadota|c__Betaproteobacteria|o__Burkholderiales|f__Comamonadaceae|g__Hydrogenophaga|s__Hydrogenophaga
#> crassostreae
#> d__Bacteria|p__Pseudomonadota|c__Alphaproteobacteria|o__Sphingomonadales|f__Sphingomonadaceae|g__Novosphingobium|s__Novosphingobium
#> sp. KCTC 2891
#> rowData names(10): Taxonomy_Full Domain ... Species Rank
#> colnames(6): SAMPLE01_CS00 SAMPLE01_CS20 ... SAMPLE01_CS80
#> SAMPLE01_CS90
#> colData names(2): Sample_ID Confidence_Score
#> reducedDimNames(0):
#> mainExpName: NULL
#> altExpNames(0):
#> rowLinks: NULL
#> rowTree: NULL
#> colLinks: NULL
#> colTree: NULL
head(SummarizedExperiment::colData(tse_object))
#> DataFrame with 6 rows and 2 columns
#> Sample_ID Confidence_Score
#> <character> <integer>
#> SAMPLE01_CS00 SAMPLE01 0
#> SAMPLE01_CS20 SAMPLE01 20
#> SAMPLE01_CS40 SAMPLE01 40
#> SAMPLE01_CS60 SAMPLE01 60
#> SAMPLE01_CS80 SAMPLE01 80
#> SAMPLE01_CS90 SAMPLE01 90
The object is also saved, together with a tab-separated copy of the matrix, in 001_imported_matrix/, where the following steps read it from.
kariocas_result objectEvery analysis step returns a kariocas_result object:
$data: the computed table;$plots: the figures, as ggplot/patchwork objects (or UpSetR plots) that can be printed or modified;$paths: the files written to the project folder.By default (export = TRUE) results are also written to the step’s sub-folder as PDF, TSV and RDS files, with a log in logs/. With export = FALSE nothing is written, which is convenient for interactive exploration, for example when comparing method or tax_level values. The final mosaic (see The final mosaic below) reads the threshold tables exported by the previous steps, so run those with export = TRUE before building it.
taxa_retention() counts, for each sample and domain, how many distinct taxa at tax_level are detected (at least one read) at each CS, and expresses it as a percentage of the maximum across CS values, usually the count at CS 0. This decay curve drops steeply while spurious assignments are removed and flattens once only well-supported taxa remain. The Stability Index (SI) engine locates that transition and reports it as the Primary SI, the optimal CS for that domain. Several strategies are available through method:
kneedle, this value is also reported as a Secondary SI when it is stricter than the elbow.retention <- taxa_retention(project_dir = proj_dir, tax_level = "Species")
retention
#> <kariocas_result> step: 002_taxa_retention
#> data : 15 row(s) x 7 column(s)
#> plots : 1 [SAMPLE_Group_Retention]
#> export: 3 file(s) in /tmp/RtmpvXuoop/karioCaS_Tutorial/002_taxa_retention
audit_df <- retention$data
audit_df[
audit_df$SI_Type %in% "Primary_SI",
c("Sample", "Domain", "CS", "Taxa_Count", "Pct_Retained", "Step_Loss_Pct")
]
#> # A tibble: 3 × 6
#> Sample Domain CS Taxa_Count Pct_Retained Step_Loss_Pct
#> <chr> <chr> <int> <int> <dbl> <dbl>
#> 1 SAMPLE01 Bacteria 60 48 70.6 22.1
#> 2 SAMPLE01 Archaea 20 3 5 95
#> 3 SAMPLE01 Eukaryota 40 7 31.8 22.7
Taxa_Count is the number of taxa detected at that CS, Pct_Retained its percentage of the maximum, and Step_Loss_Pct the percentage points lost since the previous CS. The figure overlays the curves of all samples of a biological group (groups are inferred from sample names by removing trailing digits, e.g. SAMPLE33 and SAMPLE34 form group SAMPLE); the bold line is the group mean with a +/- SD band, and the dashed line is the median Primary SI of each domain.
retention$plots[[1]]
Detailed per-sample panels (all ranks, taxa versus reads) are produced on request with detail_samples = "all" or a list of sample names.
A domain needs a decay curve with at least three CS values to be assessed. In the example data, viral species are detected only at CS 0 (16 species) and CS 0.2 (one species), so no Stability Index is reported for Viruses. If CS_V = "auto" were requested in the next step, CS 0 would be used for Viruses and a warning would say so; the example below therefore sets the viral thresholds manually.
Low-count taxa are a second source of false positives. reads_per_taxa() raises a minimum-read cutoff step by step (1, 2, 3, 4, 5, 7, 10, then log-spaced values up to the largest count) and counts how many taxa at analysis_level still have at least that many reads, at each CS. The elbow of this saturation curve, located with the same engine on a log10 read axis, is the optimal minimum number of reads for the domain at that CS.
reads <- reads_per_taxa(project_dir = proj_dir, analysis_level = "Species")
head(reads$data[reads$data$SI_Type %in% "Primary_SI", ])
#> Sample CS Domain Cutoff Taxa_Count Pct_Retained Step_Loss_Pct
#> 25 SAMPLE01 0 Bacteria 50000 6 8.823529 4.411765
#> 40 SAMPLE01 0 Archaea 70 2 3.333333 11.666667
#> 48 SAMPLE01 0 Viruses 4 8 50.000000 12.500000
#> 65 SAMPLE01 0 Eukaryota 100 6 27.272727 13.636364
#> 96 SAMPLE01 20 Bacteria 3000 10 14.705882 5.882353
#> 105 SAMPLE01 20 Archaea 2 2 66.666667 33.333333
#> SI_Type
#> 25 Primary_SI
#> 40 Primary_SI
#> 48 Primary_SI
#> 65 Primary_SI
#> 96 Primary_SI
#> 105 Primary_SI
retrieve_selected_taxa() builds, for each sample, a profile in which each domain is taken from the report at its own CS and filtered by its own minimum reads. Each threshold can be "auto" (the Primary SI or optimal reads from the exported audits), "secondary" (the more conservative value, when available) or a number. The mosaic keeps all taxonomic ranks, like the original reports.
mosaic <- retrieve_selected_taxa(
project_dir = proj_dir,
CS_B = "auto", reads_min_B = "auto", # optimal CS + optimal reads
CS_A = "secondary", reads_min_A = "auto", # conservative CS, optimal reads
CS_E = 40, reads_min_E = 10, # manual values for Eukaryota
CS_V = 0, reads_min_V = 0 # no filtering for Viruses
)
mosaic
#> <kariocas_result> step: 004_final_mosaic
#> data : 238 row(s) x 8 column(s)
#> plots : 0
#> export: 2 file(s) in /tmp/RtmpvXuoop/karioCaS_Tutorial/004_final_mosaic
# Thresholds applied to each domain and where they came from
unique(mosaic$data[, c(
"Domain", "CS", "CS_Source", "Min_Reads", "Min_Reads_Source"
)])
#> Domain CS CS_Source Min_Reads Min_Reads_Source
#> d__Archaea Archaea 40 [SI: Secondary] 4 [Reads:auto]
#> d__Bacteria Bacteria 60 [SI: Primary] 2000 [Reads:auto]
#> d__Eukaryota Eukaryota 40 [Manual] 10 [Manual]
#> d__Viruses Viruses 0 [Manual] 0 [Manual]
The mosaic is written to 004_final_mosaic/ in MPA (mpa/) and tab-separated (tsv/) formats, ready for downstream analysis.
The following functions help to inspect how individual taxa behave across CS values. Here they are run with export = FALSE, so the figures are only returned.
upset_kariocas() shows, for each sample and domain, which taxa are detected at which CS values. Taxa detected at every CS form the stable core, and taxa present only at low CS are the ones stringency removes.
ups <- upset_kariocas(proj_dir, tax_level = "Genus", export = FALSE)
ups$plots[["SAMPLE01_Bacteria_Genus"]]
heatmaps_karioCaS() shows the relative abundance (%) of the most abundant taxa surviving at a target CS, across all CS values up to that target. The remaining taxa are aggregated into rows according to the CS at which they were last detected.
hm <- heatmaps_karioCaS(
proj_dir,
analysis_rank = "Species", confidence_score = 40, top_n = 10,
export = FALSE
)
hm$plots[[1]]
taxa_resolution() compares, for the most abundant parent taxa (e.g. genera), the reads resolved to the child rank (e.g. species) with those that stay at the parent level. By default it analyses the final mosaic; here it is run on the imported data at CS 40.
res <- taxa_resolution(proj_dir, CS = 40, export = FALSE)
head(res$data)
#> # A tibble: 6 × 7
#> sample Source Parent_Name Parent_Cumulative_Total Child_Sum_Resolved
#> <chr> <chr> <chr> <dbl> <dbl>
#> 1 SAMPLE01 CS40 Novosphingobium 99135 15271
#> 2 SAMPLE01 CS40 Aquirufa 54003 11768
#> 3 SAMPLE01 CS40 Ideonella 46194 44620
#> 4 SAMPLE01 CS40 Rhodoferax 14924 4035
#> 5 SAMPLE01 CS40 Hydrogenophaga 13678 2873
#> 6 SAMPLE01 CS40 Croceicoccus 168 167
#> # ℹ 2 more variables: Parent_Exclusive <dbl>, Domain <chr>
res$plots[[1]]
When a study has several samples per biological group, group_upset() compares the taxa detected across the samples of each group and classifies them as Core (present in every sample), Shared or Unique (present in one sample only), a useful pattern to spot sample-specific signals and false positives. It needs at least two samples per group, which the example data does not have, so the call is shown without being run:
group_upset(project_dir = proj_dir)
The scientific concept, the method design and its validation are the author’s. The package code was written with the assistance of large language models, which also contributed to the formulation of some of the calculations. All code was reviewed and tested by the author.
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.5 LTS
#>
#> Matrix products: default
#> BLAS: /home/biocbuild/bbs-3.24-bioc/R/lib/libRblas.so
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.12.0 LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_GB LC_COLLATE=C
#> [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: America/New_York
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] karioCaS_0.99.16 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] tidyselect_1.2.1 dplyr_1.2.1
#> [3] farver_2.1.2 Biostrings_2.81.9
#> [5] S7_0.2.2 fastmap_1.2.0
#> [7] SingleCellExperiment_1.35.2 lazyeval_0.2.3
#> [9] digest_0.6.39 lifecycle_1.0.5
#> [11] tidytree_0.4.8 magrittr_2.0.5
#> [13] compiler_4.6.1 rlang_1.3.0
#> [15] sass_0.4.10 tools_4.6.1
#> [17] utf8_1.2.6 yaml_2.3.12
#> [19] knitr_1.52 labeling_0.4.3
#> [21] S4Arrays_1.13.2 bit_4.6.0
#> [23] DelayedArray_0.39.8 plyr_1.8.9
#> [25] xml2_1.6.0 RColorBrewer_1.1-3
#> [27] TreeSummarizedExperiment_2.21.0 abind_1.4-8
#> [29] BiocParallel_1.47.0 withr_3.0.3
#> [31] purrr_1.2.2 BiocGenerics_0.59.12
#> [33] grid_4.6.1 stats4_4.6.1
#> [35] ggplot2_4.0.3 scales_1.4.0
#> [37] tinytex_0.61 dichromat_2.0-1
#> [39] SummarizedExperiment_1.43.0 cli_3.6.6
#> [41] UpSetR_1.4.1 rmarkdown_2.32
#> [43] crayon_1.5.3 ragg_1.5.2
#> [45] treeio_1.37.1 generics_0.1.4
#> [47] otel_0.2.0 tzdb_0.5.0
#> [49] commonmark_2.0.0 ape_5.8-1
#> [51] cachem_1.1.0 stringr_1.6.0
#> [53] parallel_4.6.1 BiocManager_1.30.27
#> [55] XVector_0.53.0 matrixStats_1.5.0
#> [57] yulab.utils_0.2.5 vctrs_0.7.3
#> [59] Matrix_1.7-6 jsonlite_2.0.0
#> [61] litedown_0.11 bookdown_0.48
#> [63] IRanges_2.47.5 hms_1.1.4
#> [65] patchwork_1.3.2 S4Vectors_0.51.10
#> [67] bit64_4.8.6 magick_2.9.1
#> [69] systemfonts_1.3.2 tidyr_1.3.2
#> [71] jquerylib_0.1.4 glue_1.8.1
#> [73] codetools_0.2-20 ggtext_0.2.0
#> [75] stringi_1.8.9 gtable_0.3.6
#> [77] GenomicRanges_1.65.4 tibble_3.3.1
#> [79] pillar_1.11.1 rappdirs_0.3.4
#> [81] htmltools_0.5.9 Seqinfo_1.3.2
#> [83] R6_2.6.1 textshaping_1.0.5
#> [85] vroom_1.7.1 evaluate_1.0.5
#> [87] lattice_0.23-1 Biobase_2.73.2
#> [89] markdown_2.0 readr_2.2.0
#> [91] gridtext_0.1.6 bslib_0.12.0
#> [93] Rcpp_1.1.2 gridExtra_2.3.1
#> [95] SparseArray_1.13.4 nlme_3.1-171
#> [97] xfun_0.61 fs_2.1.0
#> [99] MatrixGenerics_1.25.0 forcats_1.0.1
#> [101] pkgconfig_2.0.3