Biodiversity Metrics Explained
On this page
Problem statement
"How biodiverse is this place?" gets answered with a species count, and a species count is the weakest thing you can compute from the data. It rises with sample size, rises with area, treats a species represented by one record the same as one represented by four thousand, and says nothing about whether those species are close relatives or unrelated survivors of different lineages.
The alternatives β evenness, Shannon and Simpson diversity, Hill numbers, turnover between places β each answer a different question, and each fails differently. Choosing among them is the analysis.
Quick answer
Compute several, from the abundances the aggregator already has:
import numpy as np, requests, time
API = "https://api.gbif.org/v1/occurrence/search"
def abundances(west, south, east, north, **params):
wkt = (f"POLYGON(({west} {south},{east} {south},{east} {north},"
f"{west} {north},{west} {south}))")
for attempt in range(6):
r = requests.get(API, params={**params, "geometry": wkt, "hasCoordinate": "true",
"facet": "speciesKey", "facetLimit": 1000,
"limit": 0}, timeout=180)
if r.status_code == 429:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
time.sleep(0.2)
return np.array([c["count"] for c in r.json()["facets"][0]["counts"]], float)
raise RuntimeError("rate limited")
def metrics(counts):
p = counts / counts.sum()
shannon = float(-(p * np.log(p)).sum())
simpson = float(1 - (p ** 2).sum())
return {"records": int(counts.sum()), "richness": len(counts),
"shannon": shannon, "hill1": float(np.exp(shannon)),
"simpson": simpson, "hill2": float(1 / (p ** 2).sum()),
"evenness": shannon / np.log(len(counts)) if len(counts) > 1 else np.nan,
"singletons": int((counts == 1).sum())}
city = abundances(-123.5, 49.0, -122.5, 49.5, taxonKey=359, country="CA")
north = abundances(-126.0, 54.0, -125.0, 55.0, taxonKey=359, country="CA")
print(f"{'metric':<12}{'near Vancouver':>16}{'central interior':>18}")
for k in metrics(city):
print(f"{k:<12}{metrics(city)[k]:>16.2f}{metrics(north)[k]:>18.2f}")
metric near Vancouver central interior
records 26390.00 150.00
richness 94.00 33.00
shannon 3.08 3.01
hill1 21.75 20.29
simpson 0.93 0.93
hill2 14.64 14.15
evenness 0.68 0.86
singletons 18.00 12.00
Richness says the Vancouver cell has nearly three times the species of the interior one. The Hill numbers β the "effective number of species" at increasing weight on common species β put them within 8% of each other, because the extra species are rare: 18 of the 94 have a single record, and all 18 together are 0.07% of the cell's records. Evenness runs the other way, and the cell with 149 records is the more even of the two.
Abundance or not, then Hill numbers with their order
1. Decide whether abundance is meaningful
Occurrence records count observations, not individuals. Abundance-weighted metrics on presence-only data describe recording effort per species as much as the species' abundance.
2. Prefer Hill numbers to raw indices
Shannon and Simpson are on different, unintuitive scales. Their Hill-number forms β exp(Shannon), 1/Simpson β are both in units of species, and can be compared with richness directly.
3. Report the order alongside the number
Hill numbers are a family indexed by q: q = 0 is richness, q = 1 weights species by their frequency, q = 2 weights the common ones more. Quoting "diversity" without q is quoting nothing.
4. Separate evenness from richness
Evenness is diversity divided by richness β how equally the records are spread. Two cells with identical richness can differ completely in it.
5. Look at the singletons
A cell whose species are mostly singletons has been under-sampled, whatever its metrics say.
6. Use turnover for the comparison between places
Alpha diversity within cells and beta diversity between them answer different questions; a map of alpha says nothing about whether neighbouring cells hold the same species.
Richness differences and beta diversity between cells
The data used on this page
Every number below comes from GBIF's API. GBIF adds and revises records daily, so the setup answers each GBIF request this page makes from the reply it got on 9 October 2026, and lets any other request through live.
import hashlib, io, zipfile
import requests
# GBIF adds and revises records daily. So that the numbers below reproduce, every GBIF request on
# this page is answered from the reply it got on 9 October 2026 (one file per request, named by a
# SHA-1 of its URL); any other request, or all of them once these lines are deleted, goes live.
SAVED = zipfile.ZipFile(io.BytesIO(requests.get(
"https://data.spatialworkflow.io/gbif-api-2026-10-09.zip", timeout=300).content))
live_get = requests.get
def saved_get(url, params=None, **kwargs):
prepared = requests.Request("GET", url, params=params).prepare()
try:
body = SAVED.read(hashlib.sha1(prepared.url.encode()).hexdigest()[:16] + ".json")
except KeyError:
return live_get(url, params=params, **kwargs)
reply = requests.models.Response()
reply.status_code, reply._content, reply.url = 200, body, prepared.url
reply.headers["Content-Type"] = "application/json"
return reply
requests.get = saved_get
Example 1 β where the richness difference comes from
for name, c in [("near Vancouver", city), ("central interior", north)]:
c = np.sort(c)[::-1]
share = c / c.sum()
print(f"{name}: {len(c)} species, {int(c.sum()):,} records")
print(f" top species holds {share[0]:.0%} of records; "
f"top five hold {share[:5].sum():.0%}")
print(f" species with 1 record: {(c == 1).sum()} "
f"({(c == 1).mean():.0%} of species, {(c[c == 1].sum() / c.sum()):.2%} of records)")
half = np.searchsorted(np.cumsum(share), 0.5) + 1
print(f" {half} species account for half the records\n")
near Vancouver: 94 species, 26,390 records
top species holds 16% of records; top five hold 48%
species with 1 record: 18 (19% of species, 0.07% of records)
6 species account for half the records
central interior: 33 species, 150 records
top species holds 15% of records; top five hold 51%
species with 1 record: 12 (36% of species, 8.00% of records)
5 species account for half the records
In both cells about five species hold half the records, and the difference between them is entirely in the tail. That is what richness measures and what the Hill numbers largely ignore β and it is the part of the data most sensitive to how long somebody looked.
Example 2 β beta diversity between neighbouring cells
def keys(west, south, east, north, **params):
wkt = (f"POLYGON(({west} {south},{east} {south},{east} {north},"
f"{west} {north},{west} {south}))")
for attempt in range(6):
r = requests.get(API, params={**params, "geometry": wkt, "hasCoordinate": "true",
"facet": "speciesKey", "facetLimit": 1000,
"limit": 0}, timeout=180)
if r.status_code == 429:
time.sleep(2 ** attempt)
continue
r.raise_for_status()
time.sleep(0.2)
return {c["name"] for c in r.json()["facets"][0]["counts"]}
raise RuntimeError("rate limited")
a = keys(-123.5, 49.0, -122.5, 49.5, taxonKey=359, country="CA") # near Vancouver
b = keys(-122.5, 49.0, -121.5, 49.5, taxonKey=359, country="CA") # one cell east
c = keys(-126.0, 54.0, -125.0, 55.0, taxonKey=359, country="CA") # far north
def jaccard(x, y):
return len(x & y) / len(x | y)
print(f"Vancouver cell: {len(a)} species; neighbour: {len(b)}; north: {len(c)}")
print(f"Jaccard similarity, adjacent cells: {jaccard(a, b):.2f}")
print(f"Jaccard similarity, 600 km apart: {jaccard(a, c):.2f}")
print(f"species unique to the northern cell: {len(c - a - b)}")
Vancouver cell: 94 species; neighbour: 79; north: 33
Jaccard similarity, adjacent cells: 0.62
Jaccard similarity, 600 km apart: 0.25
species unique to the northern cell: 4
Adjacent cells share 62% of their species and cells 600 km apart share 25%, which is a real, interpretable signal β and the northern cell contributes only four species that neither southern cell has. A map of alpha diversity would show the north as poor; the turnover says its fauna is mostly a subset, not a different community.
Example 3 β what happens to each metric as effort grows
rng = np.random.default_rng(0)
pool = np.repeat(np.arange(len(city)), city.astype(int)) # one entry per record
print(f"{'sample':>8}{'richness':>10}{'hill1':>9}{'hill2':>9}{'evenness':>10}")
for n in (50, 200, 1_000, 5_000, len(pool)):
draw = rng.choice(pool, size=min(n, len(pool)), replace=False)
counts = np.bincount(draw)
counts = counts[counts > 0].astype(float)
m = metrics(counts)
print(f"{n:>8,}{m['richness']:>10}{m['hill1']:>9.1f}{m['hill2']:>9.1f}"
f"{m['evenness']:>10.2f}")
sample richness hill1 hill2 evenness
50 18 13.5 9.9 0.90
200 35 22.4 15.8 0.87
1,000 49 22.2 15.8 0.80
5,000 73 21.6 14.4 0.72
26,390 94 21.7 14.6 0.68
Richness climbs with every extra record. The Hill numbers at q = 1 and q = 2 stabilise quickly, because the common species are found early and the rare ones barely move them. That stability is the practical argument for reporting them.
Why richness is the least robust metric
Why richness is the least robust metric
Richness counts every species once, so it is dominated by the rarest β exactly the species most sensitive to how long someone looked. Every other metric in the family down-weights them, which is why richness rises without limit as sampling continues while the higher-order metrics converge.
Why Hill numbers unify the family
Shannon's H and Simpson's D are different transformations of the same underlying quantity. Converting both to effective numbers of species β exp(H) and 1/D β puts richness, Shannon and Simpson on one axis indexed by q, so a "diversity profile" over q shows at a glance whether a community's diversity rests on common species or on rare ones.
Why abundance from occurrence records deserves suspicion
The counts in a facet are records, not individuals: a well-photographed squirrel in a city park can outnumber a whole population of something shy. Abundance-weighted metrics on this data measure a mixture of abundance, detectability and popularity, and the mixture is not separable without survey data.
Phylogenetic, functional and coverage-based measures
- Phylogenetic diversity weights species by evolutionary distinctness; two congeners are less diversity than two families.
- Functional diversity does the same with traits, and needs a trait database.
- Beta diversity splits into turnover and nestedness, which have different conservation implications.
- Zero-record cells are not zero-diversity cells; they are unsampled.
- Sample coverage (Chao) is a better standardisation than sample size for comparing cells.
- Species lists differ by taxonomy: GBIF's backbone lumps and splits differently from regional checklists.
Internal links
- How to build a species richness grid β the metric everybody starts with
- How to correct richness for sampling effort β making richness comparable
- Species occurrence data explained β what the counts count
- Sampling bias in occurrence data explained β why abundance is suspect
- A richness map is a map of sampling effort β the fix guide
- How to find which species occur in protected areas β metrics per site
FAQ
Is species richness a good biodiversity metric?
It is the simplest and the least robust: it rises with sample size and area, and treats every species equally.
What are Hill numbers?
A family of diversity measures in units of "effective number of species", indexed by q β q=0 is richness, q=1 relates to Shannon, q=2 to Simpson.
What is evenness?
How equally records are spread across species β diversity divided by richness.
Should I use abundance-weighted metrics on GBIF data?
With caution: the counts are records, not individuals, and reflect detectability and popularity.
What is beta diversity?
The turnover in species composition between places, as opposed to the diversity within one place.
Which metric should I report?
Several, with their order q, alongside the sample size they were computed from.