metabinR

About metabinR

metabinR performs abundance- and composition-based binning on metagenomic samples directly from FASTA or FASTQ files. Abundance-based binning (AB) analyzes long k-mers (k > 8); composition-based binning (CB) analyzes short k-mers (k < 8); hierarchical binning (ABxCB) chains the two.

The heavy lifting is implemented in Java and called via rJava. From v2.0.0 the R API returns an S4 MetabinResult object and accepts Biostrings / ShortRead inputs in addition to file paths.

Installation

if (!requireNamespace("BiocManager", quietly = TRUE))
    install.packages("BiocManager")
BiocManager::install("metabinR")

A JDK (Java >= 11) must be available before installing metabinR.

Preparation

JVM heap size

JVM flags are passed through the java.parameters option. The -Xmx flag controls the maximum heap: -Xmx1500M or -Xmx3G, etc. Set this before loading the package.

options(java.parameters = "-Xmx1500M")
library(metabinR)
library(ggplot2)
library(Biostrings)

To customise additional JVM flags (on top of the -Xmx heap setting), set options(metabinR.jvm.flags = c("-XX:+UseG1GC", ...)) before the package is loaded. The defaults returned by metabinR_jvm_options() already enable G1GC and string deduplication.

The MetabinResult class

Each binning function returns a MetabinResult S4 object:

  • assignments(res) — DataFrame of per-read cluster assignments.
  • nClusters(res) — number of clusters produced.
  • parameters(res) — list of parameters actually used.
  • algorithm(res) — "AB", "CB", or "ABxCB".
  • as.data.frame(res) — the v1.x tabular layout for back-compat.

The read_id column identifies each read. The column named by algorithm(res) contains its assigned bin, and columns such as AB.1 contain distances to candidate bins. Bin numbers are arbitrary labels; they do not identify genomes or abundance classes on their own.

Abundance based binning example

The toy simulated metagenome contains 26,664 Illumina reads (13,332 pairs of 2x150bp) sampled from 10 bacterial genomes across two abundance classes (high vs. low).

Abundance ground truth:

abundances <- read.table(
    system.file("extdata", "distribution_0.txt", package = "metabinR"),
    col.names = c("genome_id", "abundance", "AB_id"))

Read-level ground truth contains the genome of origin for each read. Add the abundance class of each genome for the AB evaluation:

reads.mapping <- read.delim(system.file(
    "extdata", "reads_mapping.tsv.gz", package = "metabinR"))
reads.mapping <- merge(reads.mapping,
                       abundances[, c("genome_id", "AB_id")],
                       by = "genome_id")

Run abundance-based binning with 10-mers into 2 clusters:

res.AB <- abundance_based_binning(
    system.file("extdata", "reads.metagenome.fasta.gz", package = "metabinR"),
    numOfClustersAB = 2,
    kMerSizeAB = 10,
    dryRun = FALSE,
    outputAB = "vignette"
)
res.AB
#> MetabinResult (AB)
#>   reads:     26,664
#>   clusters:  2
#>   inputs:    1 file(s)
#>     - /tmp/RtmpKcpJG7/Rinst1bc6586471a8/metabinR/extdata/reads.metagenome.fasta.gz

Inspect the assignments and summarize the observed bins. The distance summaries use only each read’s distance to its assigned bin:

head(as.data.frame(assignments(res.AB)))
#>   read_id AB        AB.1        AB.2
#> 1  S0R0/1  1 0.032307751 0.004981781
#> 2  S0R0/2  1 0.030796454 0.005614620
#> 3  S0R1/1  1 0.021667648 0.009437209
#> 4  S0R1/2  1 0.019364505 0.010336333
#> 5  S0R2/1  1 0.013742806 0.012755650
#> 6  S0R2/2  2 0.009775655 0.014416852
knitr::kable(as.data.frame(bin_summary(res.AB)), digits = 3)
bin n_reads proportion mean_distance median_distance
1 19761 0.741 0.024 0.024
2 6903 0.259 0.015 0.015

abundance_based_binning() wrote one FASTA per cluster and a k-mer count histogram:

histogram.AB <- read.table("vignette__AB.histogram.tsv", header = TRUE)
ggplot(histogram.AB, aes(x = counts, y = frequency)) +
    geom_area() +
    labs(title = "kmer counts histogram") +
    theme_bw()

Evaluate against the abundance-class ground truth. evaluate_bins() matches read_id to anonymous_read_id, so the two tables need not have the same row order. The confusion table has inferred bins as rows and known abundance classes as columns:

eval.AB <- evaluate_bins(res.AB, reads.mapping,
                         id = "anonymous_read_id", label = "AB_id")
eval.AB$confusion
#>    origin
#> bin     1     2
#>   1 18185  1576
#>   2  1891  5012
knitr::kable(as.data.frame(eval.AB$per_bin), digits = 3)
bin n_reads dominant_origin dominant_reads purity
1 19761 1 18185 0.920
2 6903 2 5012 0.726
knitr::kable(as.data.frame(eval.AB$overall), digits = 3)
n_reads n_bins n_origins weighted_purity weighted_recovery adjusted_rand_index
26664 2 2 0.87 0.87 0.519

Per-bin purity is the fraction of reads in a bin from its dominant abundance class. The adjusted Rand index (ARI) compares the two partitions without assuming that their labels correspond.

Composition based binning example

Here the known label is the bacterial genome of origin, rather than the abundance class used above.

Run composition-based binning with 4-mers into 10 clusters:

res.CB <- composition_based_binning(
    system.file("extdata", "reads.metagenome.fasta.gz", package = "metabinR"),
    numOfClustersCB = 10,
    kMerSizeCB = 4,
    dryRun = TRUE,
    outputCB = "vignette"
)

Check bin sizes and inspect reads with close competing distances. The margin is an absolute distance difference on this algorithm’s scale, not a probability. A small result can mean either that few reads are close to a boundary or that the chosen margin is too narrow:

knitr::kable(as.data.frame(bin_summary(res.CB)), digits = 3)
bin n_reads proportion mean_distance median_distance
3 7056 0.265 15354.64 15238
9 3022 0.113 15887.64 15816
10 1485 0.056 16183.90 16048
1 1055 0.040 15581.03 15334
8 7554 0.283 16462.67 16457
5 1412 0.053 16083.25 15941
4 2376 0.089 16167.66 16031
6 1343 0.050 14975.23 14754
2 867 0.033 16734.70 16682
7 494 0.019 16047.81 15718
close.reads <- ambiguous_reads(res.CB, margin = 0.05)
nrow(close.reads)
#> [1] 22
knitr::kable(head(as.data.frame(close.reads)), digits = 3)
read_id bin best_distance second_distance distance_margin
S0R181/1 3 16726 16726 0
S0R772/1 1 18002 18002 0
S0R1244/1 5 19762 19762 0
S0R2642/2 8 18712 18712 0
S0R3141/2 3 15104 15104 0
S0R3678/2 6 15440 15440 0

Evaluate by genome of origin. Best-bin recovery is the fraction of an origin’s reads found in its single largest bin; it is not genome completeness. Weighted purity and recovery summarize these counts over all evaluated reads:

eval.CB <- evaluate_bins(res.CB, reads.mapping,
                         id = "anonymous_read_id", label = "genome_id")
knitr::kable(as.data.frame(eval.CB$per_origin), digits = 3)
origin n_reads dominant_bin recovered_reads best_bin_recovery
Genome15.0 14190 3 5953 0.420
Genome3.0 5886 4 1616 0.275
Genome2.0 538 8 219 0.407
Genome12.0 3782 8 3679 0.973
Genome11.0 1186 8 1152 0.971
Genome6.0 356 7 67 0.188
Genome17.0 594 5 238 0.401
Genome22.0 8 8 6 0.750
Genome23.0 80 8 80 1.000
Genome4.0 44 7 20 0.455
knitr::kable(as.data.frame(eval.CB$overall), digits = 3)
n_reads n_bins n_origins weighted_purity weighted_recovery adjusted_rand_index
26664 10 10 0.649 0.489 0.15

Hierarchical (2-step ABxCB) binning example

res.ABxCB <- hierarchical_binning(
    system.file("extdata", "reads.metagenome.fasta.gz", package = "metabinR"),
    numOfClustersAB = 2,
    kMerSizeAB = 10,
    kMerSizeCB = 4,
    dryRun = TRUE,
    outputC = "vignette"
)
knitr::kable(as.data.frame(bin_summary(res.ABxCB)), digits = 3)
bin n_reads proportion mean_distance median_distance
1 19761 0.741 16731.08 16490
2 6903 0.259 16773.47 16304
eval.ABxCB <- evaluate_bins(res.ABxCB, reads.mapping,
                            id = "anonymous_read_id", label = "genome_id")
knitr::kable(as.data.frame(eval.ABxCB$overall), digits = 3)
n_reads n_bins n_origins weighted_purity weighted_recovery adjusted_rand_index
26664 2 10 0.62 0.904 0.298

In hierarchical results, distances for bins outside a read’s parent abundance bin are missing. ambiguous_reads() ignores those missing distances and omits reads with fewer than two finite candidates.

In-memory inputs (Biostrings / ShortRead)

Instead of a path, you can pass a DNAStringSet, QualityScaledDNAStringSet, or ShortReadQ. The Java backend reads from disk, so non-file inputs are staged to a tempfile transparently.

reads <- Biostrings::readDNAStringSet(
    system.file("extdata", "reads.metagenome.fasta.gz", package = "metabinR"))

res.AB.mem <- abundance_based_binning(
    reads,
    numOfClustersAB = 2,
    kMerSizeAB = 10,
    dryRun = TRUE
)
identical(nrow(assignments(res.AB.mem)), nrow(assignments(res.AB)))
#> [1] TRUE

Clean up files written by the AB run:

unlink("vignette__*")

Session Info

utils::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=en_US.UTF-8          
#>  [9] LC_ADDRESS=en_US.UTF-8        LC_TELEPHONE=en_US.UTF-8     
#> [11] LC_MEASUREMENT=en_US.UTF-8    LC_IDENTIFICATION=en_US.UTF-8
#> 
#> 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] Biostrings_2.81.9    Seqinfo_1.3.2        XVector_0.53.0      
#>  [4] IRanges_2.47.5       S4Vectors_0.51.10    BiocGenerics_0.59.12
#>  [7] generics_0.1.4       ggplot2_4.0.3        metabinR_2.1.1      
#> [10] BiocStyle_2.41.0    
#> 
#> loaded via a namespace (and not attached):
#>  [1] gtable_0.3.6             xfun_0.61                bslib_0.12.0            
#>  [4] hwriter_1.3.2.1          latticeExtra_0.6-31      rJava_1.0-18            
#>  [7] Biobase_2.73.2           lattice_0.23-1           vctrs_0.7.3             
#> [10] tools_4.6.1              bitops_1.1-0             parallel_4.6.1          
#> [13] tibble_3.3.1             pkgconfig_2.0.3          Matrix_1.7-6            
#> [16] checkmate_2.3.4          RColorBrewer_1.1-3       S7_0.2.2                
#> [19] cigarillo_1.3.1          lifecycle_1.0.5          compiler_4.6.1          
#> [22] farver_2.1.2             deldir_2.0-4             Rsamtools_2.29.0        
#> [25] codetools_0.2-20         htmltools_0.5.9          sys_3.4.3               
#> [28] buildtools_1.0.0         sass_0.4.10              yaml_2.3.12             
#> [31] pillar_1.11.1            crayon_1.5.3             jquerylib_0.1.4         
#> [34] BiocParallel_1.47.0      cachem_1.1.0             ShortRead_1.71.1        
#> [37] tidyselect_1.2.1         digest_0.6.39            dplyr_1.2.1             
#> [40] labeling_0.4.3           maketools_1.3.2          fastmap_1.2.0           
#> [43] grid_4.6.1               cli_3.6.6                magrittr_2.0.5          
#> [46] withr_3.0.3              scales_1.4.0             backports_1.5.1         
#> [49] rmarkdown_2.32           pwalign_1.9.1            jpeg_0.1-11             
#> [52] interp_1.1-6             otel_0.2.0               png_0.1-9               
#> [55] evaluate_1.0.5           knitr_1.52               GenomicRanges_1.65.4    
#> [58] rlang_1.3.0              Rcpp_1.1.2               glue_1.8.1              
#> [61] BiocManager_1.30.27      jsonlite_2.0.0           R6_2.6.1                
#> [64] GenomicAlignments_1.49.2