Native-range and invasive-status evidence in biofetchR

library(biofetchR)

Purpose

Occurrence records show where and when a taxon has been recorded. They do not, on their own, show whether that taxon is native, introduced, invasive or uncertain in the region being analysed. For invasion-biology workflows, this interpretation requires additional evidence.

This vignette explains how biofetchR attaches origin and invasive-status evidence to species-region data and occurrence-processing outputs. The focus is on keeping status assignment traceable rather than treating it as a hidden step. Users should be able to inspect the submitted name, any cleaned or corrected name, the recipient region, the evidence source, the assigned status and any rows that remain unresolved.

The evidence sources used in these workflows differ in scope. Some provide broad taxonomic or origin information, while others are more specific to invasion workflows. SInAS is invasion-focused because it covers invasive alien species, but those species still have native distributions. In biofetchR, SInAS-derived evidence can therefore help identify native-origin countries for taxa relevant to invasion analyses. It should not, however, be treated as a complete native-range source for all taxa, and a missing SInAS match should not be read as evidence of native, non-native or invasive status on its own.

This vignette covers how to:

The recommended workflow is to prepare names first, attach evidence second, inspect the audit output third, and apply stricter filters only when the evidence supports that choice.

Using the examples

The local examples use small tables created inside the vignette. They can be run without internet access and are intended to show how evidence fields, status labels and filtering modes behave.

Live examples are controlled by the run_live parameter and are skipped by default. When run_live = TRUE, the vignette can query supported evidence sources and write cache or audit files to the example output directory.

To run the live examples while rendering this vignette locally, use:

rmarkdown::render(
  "vignettes/biofetchR-origin-evidence.Rmd",
  params = list(run_live = TRUE)
)

Use the default version to read the workflow and run the local examples. Use the live version when you want to test evidence retrieval with provider data.

Evidence workflow overview

Origin evidence is easiest to interpret when classification and filtering are kept separate. In this vignette, evidence is attached first, status fields are created second, and filtering is treated as an optional final step.

#>   step                             stage
#> 1    1 Start with species-region records
#> 2    2                  Prepare taxonomy
#> 3    3      Attach native-range evidence
#> 4    4             Attach GRIIS evidence
#> 5    5       Assign origin-status fields
#> 6    6             Inspect audit outputs
#> 7    7          Apply optional filtering
#>                                                                            purpose
#> 1                Define the taxon and recipient country or region being evaluated.
#> 2 Clean names, apply manual corrections, and reject obvious non-taxonomic strings.
#> 3       Identify countries or regions treated as part of the taxon's native range.
#> 4          Identify species-country combinations listed as introduced or invasive.
#> 5    Classify rows using the available evidence and keep unresolved cases visible.
#> 6  Check taxonomy, evidence sources, status fields, and retained or excluded rows.
#> 7               Use stricter filters only when they match the biological question.

The important point is that a row does not have to be removed as soon as evidence is attached. It can first be labelled, reviewed and compared with other evidence fields before any filtering rule is applied.

Taxonomic preparation before status assignment

Taxon names should be prepared before they are matched to native-range or invasive-status evidence. Small differences in names can prevent relevant evidence from joining correctly. These differences can include authority strings, spelling variants, subspecies names, synonyms, open nomenclature, genus-level labels or non-taxonomic entries.

biofetchR provides taxonomy helpers for this step. They keep the submitted name, cleaned name and any reviewed correction visible, while separating names suitable for species-level matching from entries that should be reviewed or excluded. In the high-level pipelines, the same preparation step can be enabled with prepare_taxonomy = TRUE.

# Example input with deliberately mixed taxonomic quality.
# Some names are clean, some contain authority strings, some need manual
# correction, and some are not usable taxon names.
taxonomy_input <- data.frame(
  species = c(
    "Rattus rattus (Linnaeus, 1758)",
    "Sturnus vulgaris",
    "Xenopus laevis Daudin, 1802",
    "Trachemys scripta elegans",
    "Rattus sp.",
    "unknown",
    "water temperature"
  ),
  iso2c = c("GB", "DE", "FR", "GB", "GB", "GB", "FR"),
  stringsAsFactors = FALSE
)

taxonomy_input
#>                          species iso2c
#> 1 Rattus rattus (Linnaeus, 1758)    GB
#> 2               Sturnus vulgaris    DE
#> 3    Xenopus laevis Daudin, 1802    FR
#> 4      Trachemys scripta elegans    GB
#> 5                     Rattus sp.    GB
#> 6                        unknown    GB
#> 7              water temperature    FR

For many inputs, automatic preparation is enough. It can clean names, identify obvious non-taxonomic strings, flag entries that are not species-level records, and return tables showing which rows were accepted or rejected.

Manual corrections are optional. They should be used only for cases the user has reviewed, such as a known synonym, a subspecies that should be treated at species level, or a source-specific name that needs to be standardised before evidence matching.

The example below first runs the automatic preparation step, then repeats it with two reviewed manual corrections.

# First, prepare the input names automatically.
# This handles routine cleaning and creates an audit trail.
taxonomy_prepared_auto <- bf_prepare_taxa_for_gbif(
  df = taxonomy_input,
  name_col = "species",
  iso2_col = "iso2c",
  require_species_level = TRUE,
  deduplicate = TRUE
)

# Inspect the automatic taxonomy audit.
taxonomy_prepared_auto$audit
#> # A tibble: 7 × 11
#>   raw_name                 fixed_name cleaned_name was_manual_fixed is_non_taxon
#>   <chr>                    <chr>      <chr>        <lgl>            <lgl>       
#> 1 Rattus rattus (Linnaeus… Rattus ra… Rattus ratt… FALSE            FALSE       
#> 2 Sturnus vulgaris         Sturnus v… Sturnus vul… FALSE            FALSE       
#> 3 Xenopus laevis Daudin, … Xenopus l… Xenopus lae… FALSE            FALSE       
#> 4 Trachemys scripta elega… Trachemys… Trachemys s… FALSE            FALSE       
#> 5 Rattus sp.               Rattus sp. Rattus       FALSE            FALSE       
#> 6 unknown                  unknown    <NA>         FALSE            TRUE        
#> 7 water temperature        water tem… <NA>         FALSE            TRUE        
#> # ℹ 6 more variables: is_open_or_higher <lgl>, genus <chr>,
#> #   species_level_candidate <lgl>, accepted <lgl>, rejection_reason <chr>,
#> #   iso2c <chr>

# Optional: define manual overrides only for names the user has reviewed.
# These are not required for every workflow.
manual_taxonomy_fixes <- c(
  "Xenopus laevis Daudin, 1802" = "Xenopus laevis",
  "Trachemys scripta elegans" = "Trachemys scripta"
)

# Rerun taxonomy preparation with the reviewed manual overrides.
taxonomy_prepared_reviewed <- bf_prepare_taxa_for_gbif(
  df = taxonomy_input,
  name_col = "species",
  iso2_col = "iso2c",
  manual_fixes = manual_taxonomy_fixes,
  require_species_level = TRUE,
  deduplicate = TRUE
)

# Compare the reviewed audit table with the automatic audit table.
taxonomy_prepared_reviewed$audit
#> # A tibble: 7 × 11
#>   raw_name                 fixed_name cleaned_name was_manual_fixed is_non_taxon
#>   <chr>                    <chr>      <chr>        <lgl>            <lgl>       
#> 1 Rattus rattus (Linnaeus… Rattus ra… Rattus ratt… FALSE            FALSE       
#> 2 Sturnus vulgaris         Sturnus v… Sturnus vul… FALSE            FALSE       
#> 3 Xenopus laevis Daudin, … Xenopus l… Xenopus lae… TRUE             FALSE       
#> 4 Trachemys scripta elega… Trachemys… Trachemys s… TRUE             FALSE       
#> 5 Rattus sp.               Rattus sp. Rattus       FALSE            FALSE       
#> 6 unknown                  unknown    <NA>         FALSE            TRUE        
#> 7 water temperature        water tem… <NA>         FALSE            TRUE        
#> # ℹ 6 more variables: is_open_or_higher <lgl>, genus <chr>,
#> #   species_level_candidate <lgl>, accepted <lgl>, rejection_reason <chr>,
#> #   iso2c <chr>

The taxonomy audit is the main output to inspect from this step. It shows which names were accepted for matching, which were changed, and which were rejected before origin or invasive-status evidence was attached. Manual corrections should remain deliberate and documented, not a replacement for reviewing the input table.

When taxonomy preparation is used inside a pipeline run, taxonomy files are written when export_taxonomy_audit = TRUE. Keep these files with the origin-evidence outputs so the names used for matching can be checked later.

Example species-country input

The local examples below use a small species-country table. Each row defines the taxon and recipient country being evaluated.

recipient_records <- data.frame(
  species = c(
    "Rattus rattus",
    "Sturnus vulgaris",
    "Xenopus laevis",
    "Trachemys scripta",
    "Carcinus maenas"
  ),
  iso2c = c("GB", "DE", "FR", "GB", "GB"),
  stringsAsFactors = FALSE
)

recipient_records
#>             species iso2c
#> 1     Rattus rattus    GB
#> 2  Sturnus vulgaris    DE
#> 3    Xenopus laevis    FR
#> 4 Trachemys scripta    GB
#> 5   Carcinus maenas    GB

Status categories

The origin-evidence workflow uses a small set of working status categories. These categories describe how the selected evidence supports each species-country row; they should not be treated as permanent labels outside the evidence context used to create them.

#>       status
#> 1     native
#> 2 non_native
#> 3   invasive
#> 4 unresolved
#>                                                                                                                    meaning
#> 1                                                   The recipient region is supported as part of the taxon's native range.
#> 2    The taxon is interpreted as introduced or non-native, but invasive-status evidence is absent in the selected sources.
#> 3 The taxon is introduced or non-native and has invasive-status evidence, such as impact, rapid spread, or high abundance.
#> 4                                                   The available evidence is not sufficient to assign a confident status.

Positive evidence is the safest basis for interpretation. If a recipient country matches the native-range evidence, the row can be interpreted as native for that workflow. If the evidence does not support a clear status, the row should remain unresolved unless another source provides stronger support for a native, non-native or invasive-status interpretation.

Project native-range evidence

A project native-range table records which countries or regions are treated as part of each taxon’s native distribution in the current workflow. This evidence can come from curated checklists, expert-reviewed sources, previous project data, or native-origin information compiled earlier from supported web/API sources.

The table does not need to be a global native-range gazetteer. It should record the evidence being used for the analysis, along with enough source information to show where that evidence came from and what kind of source it represents. This is important because broad origin sources, project-specific checklists and invasion-focused sources can differ in coverage and purpose.

At minimum, the table should include a species name and the country codes treated as native for that species. During status assignment, these fields are compared with the recipient country in the species-country input table.

# Example project native-range evidence table.
# Each row gives the countries treated as part of the taxon's native range.
# Multiple ISO3 country codes are separated with semicolons.
native_ranges <- data.frame(
  species = c(
    "Rattus rattus",
    "Sturnus vulgaris",
    "Xenopus laevis",
    "Trachemys scripta",
    "Carcinus maenas"
  ),
  native_origin_iso3 = c(
    "IND;PAK",
    "DEU;FRA;GBR",
    "ZAF;LSO;SWZ",
    "USA;MEX",
    "GBR;IRL;FRA;ESP;PRT"
  ),
  native_origin_flag = "HAS_ORIGIN",
  native_sources_used = "example_project_table",
  stringsAsFactors = FALSE
)

native_ranges
#>             species  native_origin_iso3 native_origin_flag
#> 1     Rattus rattus             IND;PAK         HAS_ORIGIN
#> 2  Sturnus vulgaris         DEU;FRA;GBR         HAS_ORIGIN
#> 3    Xenopus laevis         ZAF;LSO;SWZ         HAS_ORIGIN
#> 4 Trachemys scripta             USA;MEX         HAS_ORIGIN
#> 5   Carcinus maenas GBR;IRL;FRA;ESP;PRT         HAS_ORIGIN
#>     native_sources_used
#> 1 example_project_table
#> 2 example_project_table
#> 3 example_project_table
#> 4 example_project_table
#> 5 example_project_table

In this example, native_origin_iso3 contains the ISO3 country codes treated as native for each species, while native_sources_used records the evidence source. For real analyses, source fields should be kept because native-range evidence can vary among providers, taxonomic treatments and spatial resolutions.

Attaching native-range evidence

Start by attaching native-range evidence in audit_only mode. This keeps every species-country row in the table and adds evidence fields that can be checked before any filtering decision is made.

native_status <- bf_attach_native_status(
  df = recipient_records,
  species_col = "species",
  iso2c_col = "iso2c",
  native_ranges = native_ranges,
  native_filter_mode = "audit_only",
  quiet = TRUE
)

native_status
#> # A tibble: 5 × 14
#>   species           iso2c native_name_key   native_name       native_origin_iso3
#>   <chr>             <chr> <chr>             <chr>             <chr>             
#> 1 Rattus rattus     GB    rattus rattus     Rattus rattus     IND;PAK           
#> 2 Sturnus vulgaris  DE    sturnus vulgaris  Sturnus vulgaris  DEU;FRA;GBR       
#> 3 Xenopus laevis    FR    xenopus laevis    Xenopus laevis    LSO;SWZ;ZAF       
#> 4 Trachemys scripta GB    trachemys scripta Trachemys scripta MEX;USA           
#> 5 Carcinus maenas   GB    carcinus maenas   Carcinus maenas   ESP;FRA;GBR;IRL;P…
#> # ℹ 9 more variables: native_origin_flag <chr>, native_has_origin <lgl>,
#> #   native_sources_used <chr>, native_recipient_iso3 <chr>,
#> #   native_is_native_recipient <lgl>, native_is_non_native_recipient <lgl>,
#> #   native_status <chr>, native_filter_keep <lgl>,
#> #   native_filter_rejection_reason <chr>

The summary gives a quick overview of how the supplied evidence was attached, including which rows received native-range support and which rows remain unresolved.

bf_native_status_summary(native_status)
#> # A tibble: 2 × 2
#>   native_status            n
#>   <chr>                <int>
#> 1 non_native_recipient     3
#> 2 native_recipient         2

For review, it is often easier to look at only the columns related to names, regions, sources, status and filtering decisions.

native_audit_cols <- unique(c(
  intersect(c("species", "iso2c"), names(native_status)),
  grep("native|origin|source|status|decision|filter", names(native_status), value = TRUE)
))

native_status[, native_audit_cols, drop = FALSE]
#> # A tibble: 5 × 14
#>   species           iso2c native_name_key   native_name       native_origin_iso3
#>   <chr>             <chr> <chr>             <chr>             <chr>             
#> 1 Rattus rattus     GB    rattus rattus     Rattus rattus     IND;PAK           
#> 2 Sturnus vulgaris  DE    sturnus vulgaris  Sturnus vulgaris  DEU;FRA;GBR       
#> 3 Xenopus laevis    FR    xenopus laevis    Xenopus laevis    LSO;SWZ;ZAF       
#> 4 Trachemys scripta GB    trachemys scripta Trachemys scripta MEX;USA           
#> 5 Carcinus maenas   GB    carcinus maenas   Carcinus maenas   ESP;FRA;GBR;IRL;P…
#> # ℹ 9 more variables: native_origin_flag <chr>, native_has_origin <lgl>,
#> #   native_sources_used <chr>, native_recipient_iso3 <chr>,
#> #   native_is_native_recipient <lgl>, native_is_non_native_recipient <lgl>,
#> #   native_status <chr>, native_filter_keep <lgl>,
#> #   native_filter_rejection_reason <chr>

At this stage, no filtering has been applied. Use the audit view to check whether the evidence joined as expected, which rows have positive native-range support, which rows remain unresolved, and whether the source fields are clear enough for later reporting.

Filtering modes

Filtering mode controls which rows are retained after native-range evidence has been attached. These modes are useful for creating analysis-specific subsets, but they should be chosen only after the audit fields have been inspected.

#>                    mode
#> 1            audit_only
#> 2       non_native_only
#> 3 non_native_or_unknown
#> 4           native_only
#>                                                            behaviour
#> 1                             Keep all rows and add evidence fields.
#> 2 Keep only rows interpreted as non-native in the recipient country.
#> 3          Keep rows interpreted as non-native plus unresolved rows.
#> 4     Keep only rows interpreted as native in the recipient country.
#>                                                        typical_use
#> 1                                               First-pass review.
#> 2                                 Strict non-native-only analysis.
#> 3 Precautionary workflows where unresolved rows need later review.
#> 4                     Native-range checks or validation workflows.

The safest first pass is audit_only, because it keeps all rows while the evidence fields are checked. Stricter modes are useful when the analysis requires a narrower subset, but unresolved rows should be handled deliberately rather than silently treated as confirmed non-native records.

# Strict non-native-only table.
non_native_only <- bf_attach_native_status(
  df = recipient_records,
  species_col = "species",
  iso2c_col = "iso2c",
  native_ranges = native_ranges,
  native_filter_mode = "non_native_only",
  quiet = TRUE
)

# Less strict table that keeps unresolved rows for later review.
non_native_or_unknown <- bf_attach_native_status(
  df = recipient_records,
  species_col = "species",
  iso2c_col = "iso2c",
  native_ranges = native_ranges,
  native_filter_mode = "non_native_or_unknown",
  quiet = TRUE
)

non_native_only
#> # A tibble: 3 × 14
#>   species           iso2c native_name_key   native_name       native_origin_iso3
#>   <chr>             <chr> <chr>             <chr>             <chr>             
#> 1 Rattus rattus     GB    rattus rattus     Rattus rattus     IND;PAK           
#> 2 Xenopus laevis    FR    xenopus laevis    Xenopus laevis    LSO;SWZ;ZAF       
#> 3 Trachemys scripta GB    trachemys scripta Trachemys scripta MEX;USA           
#> # ℹ 9 more variables: native_origin_flag <chr>, native_has_origin <lgl>,
#> #   native_sources_used <chr>, native_recipient_iso3 <chr>,
#> #   native_is_native_recipient <lgl>, native_is_non_native_recipient <lgl>,
#> #   native_status <chr>, native_filter_keep <lgl>,
#> #   native_filter_rejection_reason <chr>
non_native_or_unknown
#> # A tibble: 3 × 14
#>   species           iso2c native_name_key   native_name       native_origin_iso3
#>   <chr>             <chr> <chr>             <chr>             <chr>             
#> 1 Rattus rattus     GB    rattus rattus     Rattus rattus     IND;PAK           
#> 2 Xenopus laevis    FR    xenopus laevis    Xenopus laevis    LSO;SWZ;ZAF       
#> 3 Trachemys scripta GB    trachemys scripta Trachemys scripta MEX;USA           
#> # ℹ 9 more variables: native_origin_flag <chr>, native_has_origin <lgl>,
#> #   native_sources_used <chr>, native_recipient_iso3 <chr>,
#> #   native_is_native_recipient <lgl>, native_is_non_native_recipient <lgl>,
#> #   native_status <chr>, native_filter_keep <lgl>,
#> #   native_filter_rejection_reason <chr>

Use non_native_only only when the evidence fields provide enough support for a strict non-native subset. Use non_native_or_unknown when unresolved rows should be retained for later checking, sensitivity analysis or manual review.

Native-range evidence from web/API sources

biofetchR can compile native-origin evidence from supported web/API sources. This is useful when a project table is incomplete, when unresolved rows need additional checking, or when users want to compare evidence returned by different providers.

The current source registry includes SInAS, GBIF Species API distribution records, and WoRMS Aphia distribution records. These sources differ in scope and coverage. SInAS is invasion-focused, but can still provide native-origin countries for the invasive alien species it covers. GBIF and WoRMS provide different kinds of distribution or taxonomic-origin evidence. The returned outputs should therefore be inspected as provider evidence, not treated as a single definitive native-range authority.

# Show native-range web/API sources supported by the installed package version.
bf_available_native_web_sources()
#>   source
#> 1  sinas
#> 2   gbif
#> 3  worms
#>                                                                   description
#> 1           SInAS 3.1.1 native-location records resolved through AllLocations
#> 2 GBIF Species API distributions with native-like establishment/status fields
#> 3                  WoRMS REST Aphia distributions for marine and aquatic taxa
#>    access_type default
#> 1 zenodo_cache    TRUE
#> 2     json_api    TRUE
#> 3     json_api    TRUE

The live example below requests evidence from all selected sources. The returned object contains a retrieval summary, a species-level evidence table, a long-format provider table, and any provider strings that could not be mapped cleanly.

# Retrieve native-range evidence from the selected web/API sources.
# This is wrapped in run_live_safely() so that a temporary provider failure
# does not stop the whole vignette from rendering.
native_web <- run_live_safely(

  # Compile native-origin evidence for the species in recipient_records.
  bf_fetch_native_ranges_web(

    # Species-country table containing the taxa to query.
    species = recipient_records,

    # Column containing the taxon names.
    species_col = "species",

    # Native-range evidence sources to query.
    # These currently include SInAS, GBIF Species API distribution records,
    # and WoRMS Aphia distribution records.
    sources = c("sinas", "gbif", "worms"),

    # Cache provider results so repeated runs do not need to re-query
    # the same sources unnecessarily.
    cache_dir = file.path(output_root, "cache", "native_web"),

    # Use cached results when available.
    # Set this to TRUE only when the provider evidence should be refreshed.
    force_refresh = FALSE,

    # Pause briefly between provider requests to avoid sending rapid repeated
    # queries to external services.
    sleep_sec = 0.25,

    # Show provider messages during the live example.
    quiet = FALSE,

    # Return a structured list containing summary, species-level,
    # long-format, and unmapped evidence outputs.
    return = "list"
  )
)

# If the live query completed successfully, inspect the returned evidence.
if (!is.null(native_web)) {

  # Summary of the native-web retrieval.
  native_web$summary

  # Species-level native-range evidence table.
  # This is usually the table used for attachment to species-country records.
  native_web$species

  # Long-format provider evidence.
  # This is useful for checking which source returned which evidence.
  native_web$long

  # Provider strings or regions that could not be mapped cleanly.
  # These should be reviewed if many species remain unresolved.
  native_web$unmapped
}

For most workflows, native_web$species is the table used for status attachment. The long-format and unmapped outputs are used for checking where the evidence came from, which provider contributed it, and whether any region names failed to map cleanly.

Write the web-derived evidence to disk when it will be reused, checked later, or reported as part of an analysis.

if (exists("native_web") && !is.null(native_web)) {
  bf_write_native_web_outputs(
    native_web,
    output_dir = file.path(output_root, "native_web_audit"),
    prefix = "native_web"
  )
} else {
  skip_live_message("native web evidence was not available from the previous live chunk")
}

The species-level output can then be passed to bf_attach_native_status() in the same way as a project native-range table.

# Attach the species-level native-range evidence returned by the web/API
# retrieval step to the species-country input table.
# This chunk depends on the previous native_web object being available.
if (exists("native_web") && !is.null(native_web)) {

  # Add native-status fields to the recipient species-country table.
  native_web_status <- bf_attach_native_status(

    # Species-country table to classify.
    df = recipient_records,

    # Column containing the taxon name in the species-country table.
    species_col = "species",

    # Column containing the recipient country as an ISO2 code.
    iso2c_col = "iso2c",

    # Species-level native-range evidence returned by bf_fetch_native_ranges_web().
    # This table combines the selected web/API sources into a format that can be
    # used by bf_attach_native_status().
    native_ranges = native_web$species,

    # Column containing the taxon name in the native-range evidence table.
    native_species_col = "species",

    # Keep all rows and add native-range evidence fields for inspection.
    # No records are removed at this stage.
    native_filter_mode = "audit_only",

    # Suppress routine messages from the attachment function.
    quiet = TRUE
  )

  # Display the species-country table with attached native-range evidence.
  native_web_status

} else {

  # If the previous live retrieval step did not run or failed, print a clear
  # skip message rather than causing the vignette to fail.
  skip_live_message("native web evidence was not available from the previous live chunk")
}

The same evidence retrieval step can also be triggered directly from bf_attach_native_status(). This is useful when users do not need to edit or combine the provider table before attachment.

# Attach native-range evidence directly from supported web/API sources.
# This route is useful when the workflow should retrieve provider evidence
# during the native-status attachment step rather than supplying a prebuilt
# native_ranges table.
native_web_direct <- run_live_safely(

  # Attach native-status fields to the species-country input table.
  bf_attach_native_status(

    # Species-country table to classify.
    df = recipient_records,

    # Column containing the taxon name.
    species_col = "species",

    # Column containing the recipient country as an ISO2 code.
    iso2c_col = "iso2c",

    # No local native-range table is supplied here.
    # Instead, evidence is retrieved from the selected web/API sources below.
    native_ranges = NULL,

    # Turn on native-range evidence retrieval from supported provider sources.
    use_native_web = TRUE,

    # Query all selected native-range web/API sources.
    # SInAS, GBIF distribution metadata, and WoRMS Aphia distributions are used
    # as complementary evidence sources.
    native_web_sources = c("sinas", "gbif", "worms"),

    # Store downloaded or retrieved provider evidence in a cache directory.
    # This avoids repeating the same provider requests unnecessarily.
    native_web_cache_dir = file.path(output_root, "cache", "native_web_direct"),

    # Write native-web audit outputs so the provider evidence can be inspected.
    export_native_web_audit = TRUE,

    # Directory where native-web audit files will be written.
    native_web_audit_dir = file.path(output_root, "native_web_direct_audit"),

    # Keep all rows and attach evidence fields for review.
    # No records are removed at this stage.
    native_filter_mode = "audit_only",

    # Suppress routine messages from the attachment function.
    quiet = TRUE
  )
)

# If the live provider query completed successfully, display the attached
# native-status table.
if (!is.null(native_web_direct)) {
  native_web_direct
}

Both routes produce evidence fields that should be reviewed before filtering. Rows that remain unmatched after web/API retrieval should stay unresolved unless another source supports a clearer status interpretation.

GRIIS invasive-status evidence

The Global Register of Introduced and Invasive Species (GRIIS) provides country-, territory- and island-level evidence for taxa listed as introduced or invasive. In biofetchR, GRIIS is used to add introduced-status and invasive-status fields to species-country rows.

GRIIS evidence should be interpreted separately from native-range evidence. Native-range evidence helps assess whether the recipient country is supported as part of a taxon’s native distribution. GRIIS evidence helps identify recipient regions where the taxon has been listed as introduced or invasive.

In GRIIS, invasive-status evidence is linked to evidence of environmental impact or to source information indicating that a species is widespread, spreading rapidly or present in high abundance (Pagad et al., 2018). This makes GRIIS particularly useful for invasion workflows. However, missing GRIIS evidence should not be treated as proof that a taxon is absent, native or not invasive; it may simply mean that the species-country combination is not represented in the selected GRIIS evidence.

# Attach GRIIS introduced/invasive-status evidence to the species-country table.
# This is a live example because the function may download or retrieve GRIIS
# evidence when a local GRIIS table is not supplied.
griis_status <- run_live_safely(

  # Add GRIIS status fields to the input table.
  bf_attach_griis_status(

    # Species-country table to classify.
    df = recipient_records,

    # Column containing the taxon name.
    species_col = "species",

    # Column containing the recipient country as an ISO2 code.
    iso2c_col = "iso2c",

    # No preloaded GRIIS table is supplied here.
    # When griis = NULL, the function uses the configured GRIIS retrieval/cache
    # workflow to obtain the evidence.
    griis = NULL,

    # Directory used to cache GRIIS files or processed evidence.
    # This avoids repeating the same retrieval step unnecessarily.
    cache_dir = file.path(output_root, "cache", "griis"),

    # Use cached GRIIS evidence when available.
    # Set this to TRUE only when you deliberately want to refresh the cache.
    force_refresh = FALSE,

    # Require the species-country combination to match GRIIS evidence.
    # This is stricter than checking whether the species appears anywhere in GRIIS.
    require_country = TRUE,

    # Show progress messages for the live example.
    quiet = FALSE
  )
)

# If the GRIIS attachment step completed successfully, display the output table
# with the added GRIIS evidence fields.
if (!is.null(griis_status)) {
  griis_status
}

The attached GRIIS fields should be inspected before they are used for filtering. For example, users should check whether the match is country-specific, whether the row is listed as introduced or invasive, and whether the resulting fields match the scope of the analysis.

After inspection, a stricter table can be created by retaining only rows flagged as invasive by GRIIS. This is a filtering choice for analyses that specifically require GRIIS-supported invasive-status records. For broader evidence review, keep the full attached table and treat the GRIIS fields as part of the audit output.

# Create a stricter invasive-only table from the already attached GRIIS fields.
# The previous chunk created griis_status with columns such as griis_listed,
# griis_invasive, and griis_status. Here we simply filter that existing audit
# table rather than re-running the GRIIS attachment step.
if (exists("griis_status") && !is.null(griis_status)) {

  # Keep only rows that were flagged as invasive by the attached GRIIS evidence.
  invasive_only <- griis_status[
    griis_status$griis_invasive %in% TRUE,
    ,
    drop = FALSE
  ]

  # Display the stricter invasive-only table.
  invasive_only

} else {

  # If the GRIIS attachment chunk did not run or failed, skip cleanly.
  skip_live_message("GRIIS status was not available from the previous live chunk")
}

Origin-status audit table

After native-range and GRIIS evidence have been attached, the next step is to combine the evidence fields into one interpretable status for each species-country row. The final status should be mutually exclusive, such as native, non_native, invasive or unresolved.

Native-range evidence is used to identify rows with positive support for native status in the recipient country. GRIIS evidence is used to identify rows listed as introduced or invasive in the recipient country. These evidence fields should be read together, but they should not be treated as independent final statuses for the same row.

A compact audit table helps users see why a row received its final status. It can show the native-range result, the GRIIS result, the assigned status and the suggested review action. In real workflows, rows without clear native-range or GRIIS support should remain unresolved unless another evidence source supports a stronger interpretation.

# Example origin-status audit table.
# This is not live evidence; it illustrates how evidence fields can support
# one final status assignment for each species-country row.
origin_status_audit_example <- data.frame(
  species = c(
    "Rattus rattus",
    "Sturnus vulgaris",
    "Xenopus laevis",
    "Trachemys scripta",
    "Carcinus maenas"
  ),
  iso2c = c("GB", "DE", "FR", "GB", "GB"),
  native_range_result = c(
    "outside_native_range",
    "inside_native_range",
    "outside_native_range",
    "outside_native_range",
    "inside_native_range"
  ),
  griis_result = c(
    "listed_invasive",
    "not_listed_or_unresolved",
    "listed_invasive",
    "listed_introduced",
    "not_listed_or_unresolved"
  ),
  assigned_status = c(
    "invasive",
    "native",
    "invasive",
    "non_native",
    "native"
  ),
  review_action = c(
    "retain_for_invasive_workflow",
    "exclude_from_non_native_workflow",
    "retain_for_invasive_workflow",
    "retain_for_non_native_workflow",
    "exclude_from_non_native_workflow"
  ),
  stringsAsFactors = FALSE
)

origin_status_audit_example
#>             species iso2c  native_range_result             griis_result
#> 1     Rattus rattus    GB outside_native_range          listed_invasive
#> 2  Sturnus vulgaris    DE  inside_native_range not_listed_or_unresolved
#> 3    Xenopus laevis    FR outside_native_range          listed_invasive
#> 4 Trachemys scripta    GB outside_native_range        listed_introduced
#> 5   Carcinus maenas    GB  inside_native_range not_listed_or_unresolved
#>   assigned_status                    review_action
#> 1        invasive     retain_for_invasive_workflow
#> 2          native exclude_from_non_native_workflow
#> 3        invasive     retain_for_invasive_workflow
#> 4      non_native   retain_for_non_native_workflow
#> 5          native exclude_from_non_native_workflow

This table is illustrative. In a real audit output, the distinction between positive evidence, conflicting evidence and missing evidence should be kept visible. A row should be marked for review when the available evidence is incomplete, ambiguous, source-limited or taxonomically uncertain. Review is not a biological status; it is an audit flag that tells the user to inspect the row before applying a filter.

Origin evidence inside the batch pipeline

Origin-evidence steps can also be run inside the terrestrial and freshwater batch pipeline. This is useful when the same workflow needs to prepare names, attach evidence, process GBIF records and write occurrence outputs in one run.

The example below uses audit-first settings. Taxonomy, native-range, native-web and GRIIS evidence outputs are written for inspection, but rows are not removed only because native-range evidence is unresolved or because they are not flagged as GRIIS-invasive. Filtering can be applied later if the evidence fields support the subset needed for the analysis.

# Run the live pipeline example only when GBIF credentials are available.
if (!gbif_ready) {

  # Skip cleanly rather than failing the vignette.
  skip_live_message("set GBIF credentials to run the origin-evidence pipeline example")

} else {

  # Small species-country input table for the live example.
  origin_pipeline_input <- data.frame(
    species = c(
      "Xenopus laevis",
      "Trachemys scripta"
    ),
    iso2c = c("FR", "GB"),
    stringsAsFactors = FALSE
  )

  # Run the terrestrial/freshwater pipeline with taxonomy preparation,
  # native-range evidence, GRIIS evidence, and audit outputs enabled.
  origin_pipeline_result <- run_live_safely(
    process_gbif_terrestrial_freshwater_pipeline(

      # Species-country input table.
      df = origin_pipeline_input,

      # Output folder for this vignette example.
      output_dir = file.path(output_root, "origin_pipeline"),

      # GBIF credentials read from environment variables.
      user = gbif_user,
      pwd = gbif_pwd,
      email = gbif_email,

      # Use GADM level 1 for spatial attribution.
      region_source = "gadm",
      gadm_unit = 1,
      cache_dir_gadm = file.path(output_root, "cache", "gadm"),

      # Prepare taxon names and write taxonomy audit files.
      prepare_taxonomy = TRUE,
      export_taxonomy_audit = TRUE,

      # Attach GRIIS introduced/invasive-status evidence.
      # Do not filter to GRIIS-invasive rows in this audit-mode example.
      use_griis = TRUE,
      filter_griis_invasive = FALSE,
      griis_cache_dir = file.path(output_root, "cache", "griis"),

      # Attach native-range evidence from supported web/API sources.
      # No local native-range table is supplied in this example.
      use_native_range = TRUE,
      native_ranges = NULL,
      use_native_web = TRUE,
      native_web_sources = c("sinas", "gbif", "worms"),
      native_web_cache_dir = file.path(output_root, "cache", "native_web"),
      export_native_web_audit = TRUE,

      # Keep all rows while attaching native-range evidence.
      native_filter_mode = "audit_only",

      # Write origin-status audit outputs.
      reconcile_origin_evidence = TRUE,
      export_origin_audit = TRUE,
      export_summary = TRUE,

      # Keep the vignette example small.
      batch_size = 1,

      # Clean and thin occurrence records before export.
      apply_cleaning = TRUE,
      apply_thinning = TRUE,
      dist_km = 5,

      # Write detailed outputs to disk rather than storing every table in memory.
      return_all_results = FALSE,
      store_in_memory = FALSE,

      # Show progress messages during the live example.
      quiet = FALSE
    )
  )

  # Display the returned pipeline result object.
  origin_pipeline_result
}

After the run, inspect the taxonomy, native-web, origin-evidence and gbif_summary.csv outputs together. The evidence files explain how each species-country row was interpreted before GBIF processing, while gbif_summary.csv records what happened during occurrence download, cleaning, spatial attribution and export.

Interpreting pipeline audit outputs

After a live pipeline run, inspect the summary and evidence files before using the exported occurrence records. gbif_summary.csv shows whether each species-region row completed successfully, how many records were retained after processing, and where the occurrence files were written. The evidence files show how names, native-origin information and GRIIS fields were handled before or during the run.

# Path to the pipeline output directory.
pipeline_dir <- file.path(output_root, "origin_pipeline")

# Path to the main summary file.
summary_path <- file.path(pipeline_dir, "gbif_summary.csv")

# Read and display the most useful summary fields if the file exists.
if (file.exists(summary_path)) {

  origin_summary <- read.csv(summary_path, stringsAsFactors = FALSE)

  origin_summary[
    ,
    intersect(
      c(
        "species",
        "region_id",
        "region_type",
        "n_total",
        "n_cleaned",
        "n_thinned",
        "status",
        "fail_stage",
        "fail_reason",
        "output_file"
      ),
      names(origin_summary)
    ),
    drop = FALSE
  ]

} else {
  skip_live_message("summary file was not available from the live pipeline run")
}

The next check is whether the expected audit files were written. These may include taxonomy outputs, native-web evidence files, origin-evidence summaries and the main run summary, depending on which settings were enabled.

# List audit and summary files written by the pipeline.
# These files should be kept with the exported occurrence data.
if (dir.exists(pipeline_dir)) {
  list.files(
    pipeline_dir,
    pattern = "taxonomy|origin|native_web|griis|summary",
    full.names = TRUE
  )
} else {
  skip_live_message("pipeline output directory was not available")
}

For larger workflows, keep these files with the original input table and exported occurrence records. Together, they show which names were accepted, which evidence sources contributed to the status fields, which rows remained unresolved, which rows failed or returned no records, and which occurrence files were produced.

Final checklist

Before using an origin-classified occurrence dataset, check that the evidence and filtering decisions are clear enough to review later.

Useful checks include:

The aim is not always to force every row into a final category. A good origin-evidence workflow keeps positive evidence, missing evidence and uncertainty visible. Filtering should then be applied only when it matches the biological question and the evidence available for the taxa and regions being analysed.

Provider citations

When origin or invasive-status evidence is used in an analysis, cite the provider sources that contributed to the final evidence fields. The exact citation will depend on which sources were selected, which versions were used, and whether the evidence came from a cached file, an API query or a downloaded dataset release.

For the examples in this vignette, the main provider sources are:

Keep provider versions, access dates, cache locations and exported audit files with the project outputs. These records show which evidence sources contributed to each status assignment and make it easier to update, replace or rerun the origin-evidence workflow later.

mirror server hosted at Truenetwork, Russian Federation.