# Getting started with biobouncer

biobouncer validates biological identifiers and inputs. It answers one question, “is this a valid identifier?”, with the same verdict in R and Python. This vignette walks through the sources, the checking modes, and the validation adapters using only offline data, so every chunk here runs without a network.

    library(biobouncer)

## What can be checked

`sources()` lists the source databases biobouncer knows about, and `source_info()` returns a table with a short description of each.

    sources()
    #>  [1] "bto"             "cdd"             "chebi"           "chembl"
    #>  [5] "cl"              "clinvar"         "complexportal"   "cosmic"
    #>  [9] "dbsnp"           "doid"            "drugbank"        "ec"
    #> [13] "eco"             "efo"             "ensembl"         "gnomad"
    #> [17] "go"              "hgnc"            "hgvs"            "hp"
    #> [21] "inchikey"        "interpro"        "mgi"             "mirbase"
    #> [25] "mirbase_hairpin" "mondo"           "mp"              "ncbifam"
    #> [29] "ncbitaxon"       "ncit"            "opentargets"     "orphanet"
    #> [33] "panther"         "pato"            "pdb"             "pfam"
    #> [37] "pharmgkb"        "prints"          "prosite"         "reactome"
    #> [41] "refseq"          "rfam"            "rgd"             "smart"
    #> [45] "so"              "spdi"            "uberon"          "uniparc"
    #> [49] "uniprot"         "wikipathways"

## Pattern mode: is the string well-formed?

`pattern` mode is offline and deterministic. It checks the shape of an identifier against the source’s pattern. It does not check that the identifier exists.

`check_id()` returns a tibble with one row per input. Note that it preserves the order and length of the input, and never collapses an invalid value into a bare `FALSE`: the row tells you why.

    check_id(
      c("MONDO:0005148", "mondo:5148", "GO:0006915"),
      source_db = "mondo"
    )
    #> # A tibble: 3 × 9
    #>   input        valid normalized suggestion source_db version species how   error
    #>   <chr>        <lgl> <chr>      <chr>      <chr>     <chr>   <chr>   <chr> <chr>
    #> 1 MONDO:00051… TRUE  MONDO:000… <NA>       mondo     <NA>    <NA>    patt… <NA>
    #> 2 mondo:5148   FALSE <NA>       MONDO:000… mondo     <NA>    <NA>    patt… <NA>
    #> 3 GO:0006915   FALSE <NA>       <NA>       mondo     <NA>    <NA>    patt… <NA>

The `normalized` column holds the canonical form of a valid input. The `suggestion` column holds a best-effort correction for an invalid but mappable input, such as the lowercase prefix above.

For just the verdict, use `is_valid_id()`, which returns a logical vector.

    is_valid_id(c("P04637", "p04637"), source_db = "uniprot")
    #> [1]  TRUE FALSE

## Cache mode: does the identifier exist in a snapshot?

`cache` mode checks existence against a pinned, offline snapshot. A small `sample` snapshot ships with the package, so the examples below need no downloads. A real analysis pins a dated snapshot instead.

    check_id(
      c("MONDO:0005148", "MONDO:9999999"),
      source_db = "mondo",
      how = "cache",
      version = "sample"
    )
    #> # A tibble: 2 × 9
    #>   input        valid normalized suggestion source_db version species how   error
    #>   <chr>        <lgl> <chr>      <chr>      <chr>     <chr>   <chr>   <chr> <chr>
    #> 1 MONDO:00051… TRUE  MONDO:000… <NA>       mondo     sample  <NA>    cache <NA>
    #> 2 MONDO:99999… FALSE <NA>       <NA>       mondo     sample  <NA>    cache <NA>

`MONDO:9999999` is well-formed, so it passes `pattern` mode, but it is not in the snapshot, so it fails `cache` mode. Choosing the mode is choosing how strict you want the check to be.

Snapshots are managed with a few helpers:

    biobouncer_snapshots()
    #> # A tibble: 14 × 4
    #>    source version    n_ids location
    #>    <chr>  <chr>      <int> <chr>
    #>  1 bto    sample         7 bundled
    #>  2 chebi  sample         5 bundled
    #>  3 cl     sample         7 bundled
    #>  4 doid   sample         7 bundled
    #>  5 efo    sample         5 bundled
    #>  6 go     sample         6 bundled
    #>  7 hgnc   2026-07-07 45019 bundled
    #>  8 hgnc   sample         7 bundled
    #>  9 hp     sample         7 bundled
    #> 10 mondo  sample         6 bundled
    #> 11 mp     sample         7 bundled
    #> 12 pato   sample         7 bundled
    #> 13 so     sample         7 bundled
    #> 14 uberon sample         7 bundled

## Remote mode

`remote` mode checks existence live against a source API. It needs a network, so it is not run in this vignette, but the call looks like this:

    # Live check against the Ensembl REST API.
    check_id("ENSG00000139618", source_db = "ensembl", how = "remote")

    # existence mode uses a snapshot when one is available and otherwise
    # falls back to remote.
    check_id("MONDO:0005148", source_db = "mondo", how = "existence")

A network failure in `remote` mode raises an error. It never returns a silent `FALSE`, so a failed lookup cannot be mistaken for an absent identifier.

## Species and version awareness

Some sources are species-aware. For Ensembl, the species is encoded in the id, so `pattern` mode can reject a well-formed id that belongs to the wrong species.

    # ENSMUSG is a mouse gene id.
    is_valid_id("ENSMUSG00000059552", source_db = "ensembl", species = "mus_musculus")
    #> [1] TRUE
    is_valid_id("ENSMUSG00000059552", source_db = "ensembl", species = "homo_sapiens")
    #> [1] FALSE

`species` accepts a name such as `"homo_sapiens"` or an NCBI taxon id such as `9606`. A source that is not species-aware ignores the argument.

## HGVS variant syntax

The `hgvs` source checks the syntax of an HGVS sequence variant name. This is a grammar check in `pattern` mode. It confirms the shape of the variant. It does not check coordinates or that the variant exists.

    is_valid_id(
      c(
        "NM_004006.2:c.4375C>T",
        "NP_003997.1:p.(Gly56Ala)",
        "NM_004006.2:c.76insG"
      ),
      source_db = "hgvs"
    )
    #> [1]  TRUE  TRUE FALSE

The last one is invalid: an insertion must sit between two flanking positions, so it needs a range such as `c.76_77insG`.

## Clean a column

The most common job is not “is this one id valid” but “here is a column, which values are wrong, and can you fix the ones you can”. `report_id()` runs a check over a whole column and returns a table that prints with a one-line summary of how many values are valid, repairable, invalid, or missing.

    genes <- c("TP53", "MLL", "notagene", NA)
    report_id(genes, "hgnc", how = "cache")
    #> # biobouncer report on hgnc (cache mode): 1 valid, 1 repairable, 1 invalid, 1 missing of 4
    #> # A tibble: 4 × 9
    #>   input    valid normalized suggestion source_db version    species how   error
    #>   <chr>    <lgl> <chr>      <chr>      <chr>     <chr>      <chr>   <chr> <chr>
    #> 1 TP53     TRUE  TP53       <NA>       hgnc      2026-07-07 <NA>    cache <NA>
    #> 2 MLL      FALSE <NA>       KMT2A      hgnc      2026-07-07 <NA>    cache <NA>
    #> 3 notagene FALSE <NA>       <NA>       hgnc      2026-07-07 <NA>    cache <NA>
    #> 4 <NA>     NA    <NA>       <NA>       hgnc      2026-07-07 <NA>    cache <NA>

`MLL` is a withdrawn gene symbol, so it is invalid but repairable: its successor is `KMT2A`. `repair_id()` substitutes the fixable values and leaves valid, unmappable, and missing values untouched, so it keeps the column’s length and order and drops into `dplyr::mutate()`:

    repair_id(genes, "hgnc", how = "cache")
    #> [1] "TP53"     "KMT2A"    "notagene" NA

Calling `summary()` on a report returns the counts as a one-row table.

## Validation framework adapters

The adapters wrap the core classifier so it plugs into common validation frameworks. They never reimplement any checks. The checkmate-style adapters work out of the box:

    # TRUE when all are valid, otherwise a message.
    check_valid_id(c("MONDO:0005148", "mondo:5148"), "mondo")
    #> [1] "Must be valid mondo identifiers (pattern mode), but 1 of 2 failed, for example 'mondo:5148'"

    # A single logical.
    test_valid_id("MONDO:0005148", "mondo")
    #> [1] TRUE

`id_predicate()` returns an elementwise predicate for data-frame validation with assertr or validate:

    is_mondo <- id_predicate("mondo")
    ids <- c("MONDO:0005148", "mondo:5148", "MONDO:0018076")
    ids[is_mondo(ids)]
    #> [1] "MONDO:0005148" "MONDO:0018076"

For Shiny apps, `sv_biobouncer()` returns a shinyvalidate rule. The rule returns `NULL` for a valid input and a message otherwise:

    rule <- sv_biobouncer("mondo")
    rule("MONDO:0005148")
    #> NULL
    rule("mondo:5148")
    #> [1] "Not a valid mondo identifier"

## Generating test data

`synthesize_ids()` builds a labeled “messy column” for any source, so you can exercise a validation pipeline without hand-writing test ids. Each row carries the input, its category (valid, repairable, invalid, or missing), and the verdict fields the checker returned for it.

    rows <- synthesize_ids("mondo")
    rows[, c("input", "category", "suggestion")]
    #> # A tibble: 5 × 3
    #>   input          category   suggestion
    #>   <chr>          <chr>      <chr>
    #> 1 MONDO:0005148  valid      <NA>
    #> 2 mondo:0005148  repairable MONDO:0005148
    #> 3 MONDO:0005148! invalid    <NA>
    #> 4 <NA>           missing    <NA>
    #> 5 MONDO:0005149  valid      <NA>

    # Feed the column straight into a report.
    report_id(rows$input, "mondo")
    #> # biobouncer report on mondo (pattern mode): 2 valid, 1 repairable, 1 invalid, 1 missing of 5
    #> # A tibble: 5 × 9
    #>   input        valid normalized suggestion source_db version species how   error
    #>   <chr>        <lgl> <chr>      <chr>      <chr>     <chr>   <chr>   <chr> <chr>
    #> 1 MONDO:00051… TRUE  MONDO:000… <NA>       mondo     <NA>    <NA>    patt… <NA>
    #> 2 mondo:00051… FALSE <NA>       MONDO:000… mondo     <NA>    <NA>    patt… <NA>
    #> 3 MONDO:00051… FALSE <NA>       <NA>       mondo     <NA>    <NA>    patt… <NA>
    #> 4 <NA>         NA    <NA>       <NA>       mondo     <NA>    <NA>    patt… <NA>
    #> 5 MONDO:00051… TRUE  MONDO:000… <NA>       mondo     <NA>    <NA>    patt… <NA>

The column is deterministic and offline, and the Python `synthesize()` produces the same one. `ec`, `hgvs`, and `hgnc` have no repairable form, so they omit that category. Pass `how = "cache"` for a snapshot-mode column, where a repairable value can be a retired id that maps to its successor.

## Summary

- `pattern` mode checks shape, `cache` and `remote` check existence, and `existence` uses a snapshot first and remote as a fallback.
- `report_id()` and `repair_id()` validate and clean a whole column in one call.
- Results are vectorized and preserve input order and length. Errors are explicit.
- The same inputs give the same verdicts in the Python package, which is enforced by a shared conformance corpus.