Skip to content

Repository files navigation

birdnetprocess

Will Cornwell 2026-02-18

birdnetprocess

R-CMD-check test-coverage

birdnetprocess helps you process and visualize BirdNET detection results which can be overwhelming in their volume.

Install

# install.packages("devtools") # if needed
devtools::install_github("traitecoevo/birdnetprocess")
#> Using GitHub PAT from the git credential store.
#> Skipping install of 'birdnetprocess' from a github remote, the SHA1 (d6828172) has not changed since last install.
#>   Use `force = TRUE` to force installation

Data Requirements

BirdNET produces one output file per audio recording. The package supports both common output formats:

  • Raven selection tables (.txt, tab-delimited) — must contain Begin Time (s)
  • BirdNET Analyzer CSV output (.csv, comma-separated) — must contain Start (s) or Begin Time (s)

For automatic time processing, filenames must contain a timestamp in YYYYMMDD_HHMMSS format (e.g., SL21_20260118_112124.BirdNET.results.csv).


Use Case 1: Single Site Analysis

This is the most common scenario — you have one recorder deployed at one location and a folder of BirdNET output files. In this example the folder is detections_SL21/ containing ~46 CSV result files.

Step 1: Read the data

Use read_birdnet_folder() to read all BirdNET files in a single folder and combine them into one tibble:

library(birdnetprocess)
library(dplyr)

data <- read_birdnet_folder("detections_SL21")

When to use recursive? By default recursive = FALSE, which reads only the files directly in that folder. This is correct for standard BirdNET output which is flat. Set recursive = TRUE only if your results are split into sub-directories within the site folder.

Step 2: Quick summary

Get a snapshot of the dataset with summarise_detections():

summarise_detections(data, confidence = 0.7)
# # A tibble: 7 × 2
#   statistic                   value
#   <chr>                       <chr>
# 1 Number of species           49
# 2 Number of recordings        8042
# 3 Recording window            18 Jan 26 - 19 Jan 26
# 4 Most common species           Black Field Cricket
# 5 Peak hour                   2026-01-19 04:21:02
# 6 Average detections per day  4021
# 7 Average detections per hour 178.7111

Step 3: Visualise

Species counts — a quick bar chart of how many detections per species:

plot_species_counts(data, confidence = 0.5)

Top 10 Calls

Top 10 Calls

Activity trends with day/night shading — requires latitude, longitude, and timezone for the suncalc shading:

plot_top_species(
  data,
  n_top_species = 10,
  confidence = 0.6,
  latitude = -32.44,
  longitude = 152.24,
  tz = "Australia/Sydney"
)

Day Night Patterns

Day Night Patterns

Custom time binning — by default trends are plotted hourly. Use the unit parameter for finer resolution (any interval lubridate supports, e.g., "10 min", "30 min", "3 hours"):

plot_top_species(
  data,
  n_top_species = 5,
  confidence = 0.5,
  unit = "10 min"
)

10-Minute Trends

10-Minute Trends

Use Case 2: Multi-Site Project

When you have multiple recorders deployed across different locations, BirdNET typically outputs results into separate folders — one per site. In this example we have four site folders: detections_SL21/, detections_SL25/, detections_SL42/, and detections_SL_swamp/.

Step 1: Identify site folders

You can hard-code the paths or discover them automatically:

library(birdnetprocess)
library(dplyr)

# Option A: Hard-code folder paths
folders <- c(
  "detections_SL21", "detections_SL25",
  "detections_SL42", "detections_SL_swamp"
)

# Option B: Auto-discover folders matching a pattern
all_dirs <- list.dirs(".", full.names = FALSE, recursive = FALSE)
folders <- all_dirs[grepl("^detections_", all_dirs)]

Step 2: Read all sites

Use read_birdnet_sites() — it calls read_birdnet_folder() for each path and adds a Site column derived from the folder name:

all_data <- read_birdnet_sites(folders)

# Check the Site column
unique(all_data$Site)
# [1] "detections_SL21"    "detections_SL25"
# [3] "detections_SL42"    "detections_SL_swamp"

Step 3: Compare across sites

Use facet_by = "Site" in plot_top_species() to get side-by-side panels — one per site:

plot_top_species(
  all_data,
  n_top_species = 5,
  confidence = 0.5,
  facet_by = "Site",
  latitude = -32.44,
  longitude = 152.24,
  tz = "Australia/Sydney"
)

This produces a faceted plot with each site in its own panel, making it easy to compare species activity across locations.


Use Case 3: Range-filtering a site & one-shot reports

BirdNET often proposes species that don’t actually occur at the recorder’s location — geographic false positives (e.g. eastern/coastal birds at an arid inland site). If you have a species-distribution-model raster stack (one range-masked abundance layer per species, named in lower_case_underscore style), filter_by_range() drops detections whose location falls outside a species’ modelled range.

library(birdnetprocess)
library(terra)

data <- read_birdnet_folder("wilddeserts")
abund <- terra::rast("nsw_abundance_stack_3km.tif")

filtered <- filter_by_range(
  data, abund,
  latitude = -29.229286, longitude = 141.286856
)

# Which species were dropped / kept / not assessed?
attr(filtered, "range_report")

The rule: a species is removed only if it has a matching range-map layer and the site is outside it. Non-birds (insects, mammals, anthropogenic labels) and birds with no layer (e.g. arid specialists) are always kept and flagged unassessed: no range map. Use name_overrides for species the name-matcher misses, and min_abundance to also prune very-low-probability species.

site_report() — report + plot set in one call

site_report() wraps the whole workflow: it range-filters (if you pass a raster), summarises, builds the standard plot set, and — when given output_dir — writes the plots plus a species_filter_report.csv to disk.

res <- site_report(
  data,
  latitude = -29.229286, longitude = 141.286856,
  abundance_raster = abund,
  tz = "Australia/Sydney",
  output_dir = "wilddeserts_plots/filtered"
)

res$report         # per-species kept/removed/unassessed table
res$summary        # summarise_detections() output
res$plots$daynight # ggplot objects, also saved as PNGs

Range filtering is optional — omit abundance_raster to get the summary and plots with no filtering. The abundance raster is supplied by you (it is not bundled), and terra is only needed when you range-filter.

If the recorder was running before it reached the site (e.g. test recordings made elsewhere), pass start_after / end_before to trim to the actual deployment window — these run before range filtering and also remove non-bird sounds (voices, vehicles) that range filtering leaves in:

site_report(
  data,
  latitude = -29.229286, longitude = 141.286856,
  abundance_raster = abund, tz = "Australia/Sydney",
  start_after = "2026-05-26"
)

Quick Reference: Which function should I use?

Scenario Function recursive Adds Site?
One folder of BirdNET results read_birdnet_folder() FALSE (default) No
One folder with sub-directories inside read_birdnet_folder() TRUE No
Multiple site folders for comparison read_birdnet_sites() FALSE (default) Yes

Rule of thumb:

  • If you’re working with one site, use read_birdnet_folder().
  • If you’re comparing sites, use read_birdnet_sites() — it gives you the Site column you need for facet_by = "Site" in plotting functions.

Dependencies

Dependencies (lubridate and ggplot2 are key) should be installed automatically when installing birdnetprocess from GitHub.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages