Will Cornwell 2026-02-18
birdnetprocess helps you process and visualize BirdNET detection
results which can be overwhelming in their volume.
# 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 installationBirdNET produces one output file per audio recording. The package supports both common output formats:
- Raven selection tables (
.txt, tab-delimited) — must containBegin Time (s) - BirdNET Analyzer CSV output (
.csv, comma-separated) — must containStart (s)orBegin Time (s)
For automatic time processing, filenames must contain a timestamp in
YYYYMMDD_HHMMSS format (e.g.,
SL21_20260118_112124.BirdNET.results.csv).
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.
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.
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.7111Species counts — a quick bar chart of how many detections per species:
plot_species_counts(data, confidence = 0.5)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"
)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"
)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/.
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)]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"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.
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() 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 PNGsRange 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"
)| 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 theSitecolumn you need forfacet_by = "Site"in plotting functions.
Dependencies (lubridate and ggplot2 are key) should be installed
automatically when installing birdnetprocess from GitHub.


