This vignette demonstrates how Traditional Ecological Knowledge (TEK) can be incorporated into decision models using Bayesian Networks (BNs) and Monte Carlo sampling. It is designed as a short tutorial showing practical, reproducible code snippets and guidance on eliciting and encoding TEK.
Note: this vignette is both pedagogical and practical. It gives worked examples you can run locally. The goal is to show how TEK — whether collected as counts (k of n informants), multinomial categories, or qualitative rankings (high/med/low) — can be translated into probability distributions that feed Bayesian Networks. Monte Carlo sampling is then used to propagate TEK-derived uncertainty through the BN and produce decision-relevant outcome distributions.
The examples use real survey data, homegardens: plants
and their uses in 102 homegardens in southwestern Uganda, one garden per
informant (Whitney et al. 2017; Whitney et al.
2018). Please cite both papers when you use these data. Items the
survey did not collect (informant confidence, management judgements) are
simulated or illustrative and labelled as such. For heavier examples you
may conditionally skip chunks if packages are not installed (see notes
in chunks). By default the main code chunks here are enabled so the
vignette will run in an environment with the required packages
installed.
We will show examples that use the following packages (install if you want to run the code):
You can install them with
install.packages(c("bnlearn","gRain","dplyr","ggplot2","purrr"))
We often collect TEK as counts (k informants out of n) or qualitative categories (high/medium/low). A convenient approach is to convert counts into Beta (binary) or Dirichlet (categorical) priors and then sample from these priors inside a Monte Carlo loop to create Conditional Probability Tables (CPTs) for a Bayesian Network (BN).
Below we illustrate the workflow with a tiny, didactic model: a
management Decision (Harvest vs Conserve), a latent
Abundance node (High/Low), and an Impact node
(Recovery/Decline). The goal is to compare management options under TEK
uncertainty.
library(ethnobotanyR)
# Informants are gardens. k of n gardens growing avocado (Persea americana) sell it.
avocado <- homegardens[homegardens$sp_name == "Persea americana", ]
tek_k <- sum(avocado$sale) # gardens that sell avocado
tek_n <- nrow(avocado) # gardens that grow avocado
# Beta posterior with a uniform prior: Beta(k+1, n-k+1)
alpha <- tek_k + 1
beta <- tek_n - tek_k + 1
# draw Monte Carlo samples for that probability
set.seed(123)
p_samples <- rbeta(1000, alpha, beta)
# Inspect distribution
library(ggplot2)
ggplot(data.frame(p = p_samples), aes(p)) +
geom_density() +
labs(x = "P(a garden growing avocado sells it)", title = "Beta posterior from survey counts")The avocado gardens give a small informant-level data set: - a binary response, whether the garden sells avocado (from the survey), - a categorical response, the breadth of avocado uses reported (1, 2 or 3+ uses; from the survey), and - a confidence score (1-5). The survey did not collect confidence, so this column is simulated to illustrate weighting.
set.seed(42)
uses <- setdiff(names(homegardens), c("informant", "sp_name"))
informants <- data.frame(
id = avocado$informant,
sells = avocado$sale,
breadth = cut(rowSums(avocado[uses]), c(0, 1, 2, Inf), labels = c("1", "2", "3+")),
confidence = sample(3:5, nrow(avocado), replace = TRUE, prob = c(0.2, 0.6, 0.2)) # simulated
)
knitr::kable(head(informants, 8))| id | sells | breadth | confidence |
|---|---|---|---|
| BUR1 | 1 | 2 | 3 |
| BUR10 | 1 | 2 | 3 |
| BUR11 | 0 | 1 | 4 |
| BUR2 | 0 | 1 | 3 |
| BUR3 | 0 | 1 | 5 |
| BUR4 | 0 | 1 | 4 |
| BUR5 | 0 | NA | 5 |
| BUR6 | 0 | 1 | 4 |
We use these data to illustrate aggregation approaches below: simple counts to Beta/Dirichlet, and weighted pooling by confidence.
High-level algorithm
if (requireNamespace("bnlearn", quietly = TRUE) && requireNamespace("gRain", quietly = TRUE)) {
library(bnlearn); library(gRain); library(purrr)
} else {
message("Skipping BN + gRain workflow: 'bnlearn' and/or 'gRain' not available")
}## Loading required package: gRbase
##
## Attaching package: 'gRbase'
## The following objects are masked from 'package:bnlearn':
##
## ancestors, children, nodes, parents
# ILLUSTRATIVE elicited judgement, not in the survey: 7 of 10 informants say harvest lowers abundance
tek_k <- 7 # number of informants saying 'decline'
tek_n <- 10 # total informants
alpha <- tek_k + 1
beta <- tek_n - tek_k + 1
# Structure: Decision -> Abundance -> Impact (Decision also acts on Impact)
# p_low_harvest is the probability that abundance is Low under harvest. The Beta prior
# below is uncertainty about that probability, taken here from informants' judgements;
# the share of informants who agree is a stand-in, not an observed frequency.
yn <- c("Yes", "No")
sample_one <- function(){
p_low_harvest <- rbeta(1, alpha, beta)
p_low_conserve <- 0.1 # toy value, fixed
cpt_decision <- cptable(~Decision, values = c(0.5, 0.5), levels = c("Harvest", "Conserve"))
cpt_abundance <- cptable(~Abundance | Decision,
values = c(1 - p_low_harvest, p_low_harvest, 1 - p_low_conserve, p_low_conserve),
levels = c("High", "Low"))
# Impact | Abundance, Decision: 8 values, ordered Recovery/Decline for each
# combination, Abundance varying fastest, then Decision
cpt_impact <- cptable(~Impact | Abundance + Decision,
values = c(0.95, 0.05, 0.40, 0.60, # Harvest: High, Low
0.98, 0.02, 0.70, 0.30), # Conserve: High, Low
levels = c("Recovery", "Decline"))
grain_net <- grain(compileCPT(list(cpt_decision, cpt_abundance, cpt_impact)))
# set each decision as evidence and read P(Recovery)
sapply(c("Harvest", "Conserve"), function(d) {
querygrain(setEvidence(grain_net, nodes = "Decision", states = d),
nodes = "Impact", type = "marginal")$Impact["Recovery"]
})
}
set.seed(1)
results <- as.data.frame(t(replicate(500, sample_one())))
colnames(results) <- c("Harvest", "Conserve")
# posterior of P(Recovery) per decision, and of the difference
apply(results, 2, quantile, probs = c(0.1, 0.5, 0.9))## Harvest Conserve
## 10% 0.4967453 0.952
## 50% 0.5782777 0.952
## 90% 0.6872939 0.952
## 10% 50% 90%
## 0.2647061 0.3737223 0.4552547
When you have data rather than judgements, learn the structure and
fit the parameters from it. Here each row is a species in a garden;
nodes are life form, native status and three uses (Whitney et al. 2018).
boot.strength() shows how stable each arc is across
resamples; arcs below about 0.5 should not be interpreted.
library(bnlearn)
hg <- merge(homegardens, homegardens_species, by = "sp_name")
hg <- hg[hg$type != "Palm", ]
bn_data <- data.frame(
type = factor(hg$type), native = factor(hg$native, labels = c("introduced", "native")),
food = factor(hg$food), sale = factor(hg$sale), medicine = factor(hg$medicine))
set.seed(1)
strength <- boot.strength(bn_data, R = 200, algorithm = "hc")
strength[strength$strength > 0.5 & strength$direction > 0.5, ]## from to strength direction
## 5 native type 1.000 0.6350000
## 6 native food 1.000 0.5050000
## 8 native medicine 0.840 0.7410714
## 9 food type 1.000 0.6000000
## 12 food medicine 1.000 0.6300000
## 13 sale type 0.940 0.8430851
## 14 sale native 0.585 0.7820513
## 15 sale food 0.720 0.8055556
## 16 sale medicine 0.995 0.8341709
net <- averaged.network(strength, threshold = 0.5)
fitted_net <- bn.fit(net, bn_data, method = "bayes", iss = 10) # Dirichlet priorQuery the fitted network, for example how likely a native tree is to have a medicinal use:
g <- bnlearn::as.grain(fitted_net)
q <- gRain::querygrain(gRain::setEvidence(g, nodes = c("type", "native"),
states = c("Tree", "native")),
nodes = "medicine", type = "marginal")
q$medicine## medicine
## 0 1
## 0.5524984 0.4475016
Rows are species-in-gardens, so the same garden or species appears many times and these intervals are too narrow. Treat this as exploration and use a mixed model (see the modeling vignette) for inference.
Visualization ideas:
We now show practical aggregation methods using the
informants data above.
# Binary: counts for 'sells'
k <- sum(informants$sells)
n <- nrow(informants)
alpha <- k + 1; beta <- n - k + 1
cat(sprintf("P(sells) prior ~ Beta(%d, %d) -> mean = %.2f\n", alpha, beta, alpha/(alpha+beta)))## P(sells) prior ~ Beta(22, 71) -> mean = 0.24
# Multinomial: counts for breadth categories -> Dirichlet
br_counts <- table(informants$breadth)
br_dirichlet_alpha <- as.numeric(br_counts) + 1
cat("Breadth counts:\n")## Breadth counts:
##
## 1 2 3+
## 57 22 6
Confidence is simulated here; use it only if you collect it.
# Use confidence as weight
weights <- informants$confidence / sum(informants$confidence)
# Weighted proportion, with the effective sample size (Kish) so that
# weighting does not make the prior look more certain than n informants allow
p_w <- sum(informants$sells * weights)
n_eff <- 1 / sum(weights^2)
walpha <- p_w * n_eff + 1; wbeta <- (1 - p_w) * n_eff + 1
cat(sprintf("Weighted Beta prior approx: Beta(%.1f, %.1f), n_eff = %.1f -> mean = %.2f\n", walpha, wbeta, n_eff, walpha/(walpha+wbeta)))## Weighted Beta prior approx: Beta(20.7, 69.7), n_eff = 88.5 -> mean = 0.23
# Weighted multinomial: use weighted frequencies
weighted_br <- tapply(weights, informants$breadth, sum)
weighted_br_counts <- weighted_br * n_eff
cat("Weighted breadth pseudo-counts:\n")## Weighted breadth pseudo-counts:
## 1 2 3+
## 56.314976 20.478173 5.363331
Do not map response categories to pseudo-counts (e.g. High = 5, Low = 1): that reweights the categories, not the informants, and inflates the sample size. Weight informants as in method 2, and use the Dirichlet with the effective sample size.
w <- informants$confidence / sum(informants$confidence)
p_br <- tapply(w, informants$breadth, sum)
qual_alpha <- p_br * n_eff + 1
print(round(qual_alpha, 2))## 1 2 3+
## 57.31 21.48 6.36
All three approaches give you numeric (pseudo-)counts that can be used as Beta or Dirichlet parameters. Which is appropriate depends on your elicitation protocol and whether you want to weight informants by confidence or expertise.
This appendix provides a practical TEK elicitation form template and a self-contained simulated dataset so you can reproduce all examples in this vignette without external data.
Below is a template form that can be adapted for your specific research context. The form is designed to elicit three types of information: (1) binary management perception, (2) categorical abundance assessment, and (3) confidence rating.
Informant ID: ___________
Date of interview: __________
Location / Community: ____________________
Years of experience with this resource: ___________
Primary use of resource: [ ] Subsistence [ ] Commercial [ ] Cultural [ ] Other: _______
Language of interview: [ ] English [ ] Other: ________________
Interviewer name: ____________________
“In your experience, when people harvest [RESOURCE], does this cause the population to decline quickly?”
[ ] Yes, harvesting causes the population to decline
[ ] No, the population does not decline from harvesting
[ ] Uncertain / Depends on context
“How would you assess the current abundance of [RESOURCE] in this area compared to your childhood or earlier years?”
[ ] High - abundance is good, similar to or better than before
[ ] Medium - abundance has declined somewhat, but the resource is still available
[ ] Low - abundance has declined significantly; the resource is now scarce or rare
[ ] Not sure
“What management approach do you think would be best for [RESOURCE]?”
[ ] Harvest freely (no restrictions)
[ ] Harvest with guidelines (seasonal or quantity limits)
[ ] Conserve strictly (minimize or prohibit harvesting)
[ ] Let it recover, then harvest sustainably
[ ] Other: _______________________
“How confident are you in your answers to the above questions?”
Confidence score (1 = not confident, 5 = very confident): [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
“Are there any other factors, changes, or context we should know about?”
___________________________________________________________
___________________________________________________________
___________________________________________________________
Below we generate a realistic simulated TEK dataset with 50 informants. You can use this dataset to run all the examples in the vignette and adapt them to your own data.
# Set random seed for reproducibility
set.seed(42)
# Set number of informants
n_informants <- 50
# Create a data frame with informant characteristics
# Create informant IDs from 1 to n_informants
tek_survey_data <- data.frame(
informant_id = 1:n_informants,
# Generate years of experience from normal distribution: mean=25, sd=15, minimum=5
# rnorm: draw from normal distribution with specified mean and standard deviation
# pmax: ensure no values below 5
years_experience = pmax(5, round(rnorm(n_informants, mean = 25, sd = 15))),
# Generate primary use category: subsistence, commercial, or cultural
# sample: take a sample of the specified size from the elements without replacement
primary_use = sample(
x = c("Subsistence", "Commercial", "Cultural"),
size = n_informants,
replace = TRUE,
prob = c(0.5, 0.2, 0.3)
),
# Initialize columns for responses (will fill in loops below)
says_harvest_declines = NA,
abundance_status = NA,
management_recommendation = NA,
confidence = NA
)
# Fill binary outcome: probability of saying "harvesting causes decline" increases with experience
# Loop through each informant
for (i in 1:n_informants) {
# Calculate probability based on years of experience (ranges from ~0.3 to 0.7)
p_decline <- 0.3 + (tek_survey_data$years_experience[i] / 100) * 0.4
# rbinom: draw from binomial distribution with size=1 (binary outcome) and probability p_decline
tek_survey_data$says_harvest_declines[i] <- rbinom(1, size = 1, prob = p_decline)
}
# Fill categorical abundance assessment: correlated with perception of harvest decline
# Create a matrix of probabilities for abundance (High, Medium, Low) given each response
# rows: "says decline"=1, "does not say decline"=0; columns: High, Medium, Low probabilities
abundance_probs <- matrix(
c(0.3, 0.4, 0.3, # if says decline: P(High)=0.3, P(Medium)=0.4, P(Low)=0.3
0.5, 0.3, 0.2), # if does not say decline: P(High)=0.5, P(Medium)=0.3, P(Low)=0.2
nrow = 2,
byrow = TRUE
)
# Loop through each informant to assign abundance status
for (i in 1:n_informants) {
# Select probability row based on their answer about decline (add 1 to convert 0/1 to row 1/2)
row_idx <- tek_survey_data$says_harvest_declines[i] + 1
# sample: select one abundance category with probabilities from the appropriate row
tek_survey_data$abundance_status[i] <- sample(
x = c("High", "Medium", "Low"),
size = 1,
prob = abundance_probs[row_idx, ]
)
}
# Fill management recommendation: correlated with perception of decline and current abundance
# Loop through each informant
for (i in 1:n_informants) {
# If they say decline AND perceive low abundance: recommend conservation
if (tek_survey_data$says_harvest_declines[i] == 1 &&
tek_survey_data$abundance_status[i] == "Low") {
# sample: select one management option with specified probabilities
tek_survey_data$management_recommendation[i] <- sample(
c("Conserve strictly", "Harvest with guidelines", "Let it recover, then harvest"),
size = 1,
prob = c(0.6, 0.3, 0.1)
)
# Else if they perceive high abundance: less restrictive recommendations
} else if (tek_survey_data$abundance_status[i] == "High") {
tek_survey_data$management_recommendation[i] <- sample(
c("Harvest freely", "Harvest with guidelines", "Conserve strictly"),
size = 1,
prob = c(0.4, 0.4, 0.2)
)
# Otherwise: balanced distribution of recommendations
} else {
tek_survey_data$management_recommendation[i] <- sample(
c("Harvest freely", "Harvest with guidelines", "Conserve strictly", "Let it recover, then harvest"),
size = 1,
prob = c(0.2, 0.4, 0.25, 0.15)
)
}
}
# Fill confidence scores: higher for more experienced informants
# Loop through each informant
for (i in 1:n_informants) {
# Calculate probability of high confidence based on years of experience
p_high_conf <- 0.3 + (tek_survey_data$years_experience[i] / 100) * 0.3
# Assign confidence based on this probability
# runif: draw from uniform distribution (0 to 1)
if (runif(1) < p_high_conf) {
# High confidence: choose between 4 or 5
tek_survey_data$confidence[i] <- sample(4:5, 1, prob = c(0.4, 0.6))
} else {
# Lower confidence: choose between 2 or 3
tek_survey_data$confidence[i] <- sample(2:3, 1, prob = c(0.4, 0.6))
}
}
# Display first 10 rows of the dataset
cat("TEK Survey Dataset - First 10 rows:\n\n")## TEK Survey Dataset - First 10 rows:
##
##
## | informant_id| years_experience|primary_use | says_harvest_declines|abundance_status |management_recommendation | confidence|
## |------------:|----------------:|:-----------|---------------------:|:----------------|:-------------------------|----------:|
## | 1| 46|Cultural | 1|Low |Conserve strictly | 3|
## | 2| 17|Subsistence | 0|Low |Harvest with guidelines | 5|
## | 3| 30|Subsistence | 1|Low |Conserve strictly | 2|
## | 4| 34|Subsistence | 0|Low |Conserve strictly | 5|
## | 5| 31|Commercial | 1|High |Harvest with guidelines | 5|
## | 6| 23|Commercial | 1|High |Harvest freely | 4|
## | 7| 48|Cultural | 0|High |Harvest with guidelines | 3|
## | 8| 24|Cultural | 0|Low |Conserve strictly | 4|
## | 9| 55|Cultural | 1|High |Harvest freely | 4|
## | 10| 24|Subsistence | 1|High |Harvest freely | 2|
##
##
## Summary of TEK Survey Data (n= 50 )
## ================================
# mean: calculate average; round to 1 decimal place
# min/max: find minimum and maximum values
cat("Years of experience: Mean =", round(mean(tek_survey_data$years_experience), 1),
", Range =", min(tek_survey_data$years_experience), "-",
max(tek_survey_data$years_experience), "\n")## Years of experience: Mean = 26 , Range = 5 - 59
## Primary use breakdown:
##
## Commercial Cultural Subsistence
## 12 20 18
##
##
## Binary outcome (says harvesting declines):
##
## 0 1
## 29 21
##
##
## Abundance status:
##
## High Low Medium
## 18 18 14
##
##
## Management recommendations:
##
## Conserve strictly Harvest freely
## 14 10
## Harvest with guidelines Let it recover, then harvest
## 21 5
##
##
## Confidence scores:
##
## 2 3 4 5
## 11 20 7 12
You can now apply the three aggregation approaches from Section 5.1 to this complete dataset:
# === Simple pooling: binary outcome ===
# sum: count the total number of informants who said harvesting causes decline (1 = yes, 0 = no)
k <- sum(tek_survey_data$says_harvest_declines)
# nrow: count the total number of rows (informants)
n <- nrow(tek_survey_data)
# Beta distribution parameters with Laplace smoothing (add 1 to each)
alpha_simple <- k + 1
beta_simple <- n - k + 1
cat("Binary outcome aggregation (Simple pooling):\n")## Binary outcome aggregation (Simple pooling):
# sprintf: format and print the Beta distribution parameters
cat(sprintf(" P(harvesting causes decline) ~ Beta(%d, %d)\n", alpha_simple, beta_simple))## P(harvesting causes decline) ~ Beta(22, 30)
# Calculate mean probability of the Beta distribution: alpha / (alpha + beta)
cat(sprintf(" Mean probability: %.3f\n", alpha_simple / (alpha_simple + beta_simple)))## Mean probability: 0.423
# === Simple pooling: categorical outcome (abundance) ===
# table: count frequency of each abundance category
abundance_counts <- table(tek_survey_data$abundance_status)
# Convert counts to Dirichlet parameters by adding 1 to each count
abundance_alpha <- as.numeric(abundance_counts) + 1
cat("\nAbundance status aggregation (Simple pooling):\n")##
## Abundance status aggregation (Simple pooling):
## Category counts:
##
## High Low Medium
## 18 18 14
## Dirichlet alpha (for Dirichlet prior):
## [1] 19 19 15
# === Weighted pooling: by informant confidence ===
# Divide each confidence score by the sum to create normalized weights (sum to 1)
weights <- tek_survey_data$confidence / sum(tek_survey_data$confidence)
# Weighted proportion with the Kish effective sample size
p_w_decline <- sum(tek_survey_data$says_harvest_declines * weights)
n_eff_tek <- 1 / sum(weights^2)
walpha_decline <- p_w_decline * n_eff_tek + 1
wbeta_decline <- (1 - p_w_decline) * n_eff_tek + 1
cat("\nBinary outcome aggregation (Confidence-weighted):\n")##
## Binary outcome aggregation (Confidence-weighted):
## P(harvesting causes decline) ~ Beta(19.4, 28.0)
# Calculate mean probability of the weighted Beta distribution
cat(sprintf(" Mean probability: %.3f\n", walpha_decline / (walpha_decline + wbeta_decline)))## Mean probability: 0.410
[RESOURCE] with the actual resource you are studying (e.g.,
“yam,” “mahogany,” “mangrove”).If you collect data using a form similar to the template in Section A, you can load and process it similarly to the simulated dataset:
# Load your survey data (replace 'your_data.csv' with your file)
# read.csv: read a comma-separated values file and create a data frame
my_tek_data <- read.csv("your_data.csv")
# Check structure of your data
# str: display internal structure of an R object, showing column names, types, and sample values
str(my_tek_data)
# Summarize binary outcome
cat("Binary outcome:\n")
# table: create a frequency table of the responses
print(table(my_tek_data$says_harvest_declines))
# Aggregate binary outcome using the methods from Section 5.1
# sum: count the number of "yes" responses (1 = yes)
k <- sum(my_tek_data$says_harvest_declines)
# nrow: count the total number of rows (informants)
n <- nrow(my_tek_data)
# Calculate Beta distribution parameters with Laplace smoothing
alpha <- k + 1; beta <- n - k + 1
# sprintf: format the prior as a string and print it
cat(sprintf("Prior: Beta(%d, %d)\n", alpha, beta))