ecological-agent-skills 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_CONTEXT.md +191 -0
- package/CATALOG.md +329 -0
- package/LICENSE +692 -0
- package/README.md +347 -0
- package/bin/install.mjs +168 -0
- package/docs/comparison-with-alternatives.md +38 -0
- package/docs/global-examples-index.md +103 -0
- package/docs/repository-statistics.md +101 -0
- package/docs/theoretical-foundations.md +188 -0
- package/environment.yaml +106 -0
- package/examples/community/arctic_tundra_vegetation_example.md +247 -0
- package/examples/community/bird_landuse_example.md +63 -0
- package/examples/community/phytoplankton_reservoir_example.md +60 -0
- package/examples/community/reef_fish_indopacific_example.md +221 -0
- package/examples/impact/baci_road_example.md +57 -0
- package/examples/impact/ecosystem_services_atlantic_forest.md +83 -0
- package/examples/impact/forest_loss_borneo_timeseries_example.md +225 -0
- package/examples/occupancy/puma_camera_example.md +61 -0
- package/examples/occupancy/snow_leopard_himalayas_example.md +204 -0
- package/examples/reproducible/whittaker_biome_sdm_example.md +406 -0
- package/examples/sdm/anteater_cerrado_example.md +69 -0
- package/examples/sdm/jaguar_amazon_example.md +80 -0
- package/examples/sdm/koala_climate_change_example.md +170 -0
- package/examples/sdm/wolf_recolonization_europe_example.md +193 -0
- package/package.json +43 -0
- package/renv.lock +194 -0
- package/skills/SKILL_INDEX.json +1020 -0
- package/skills/acoustic-monitoring/SKILL.md +163 -0
- package/skills/acoustic-monitoring/examples/example-prompts.md +100 -0
- package/skills/acoustic-monitoring/examples/temperate_forest_birds_example.md +285 -0
- package/skills/acoustic-monitoring/resources/acoustic-indices-reference.md +93 -0
- package/skills/acoustic-monitoring/resources/soundscape-ecology-guide.md +90 -0
- package/skills/acoustic-monitoring/resources/species-id-tools-comparison.md +89 -0
- package/skills/acoustic-monitoring/scripts/batch_species_detection.py +360 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.R +235 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.py +374 -0
- package/skills/biostatistics-workbench/SKILL.md +140 -0
- package/skills/biostatistics-workbench/examples/example-prompts.md +39 -0
- package/skills/biostatistics-workbench/resources/effect-size-reference.md +81 -0
- package/skills/biostatistics-workbench/resources/glm-family-link-reference.md +47 -0
- package/skills/biostatistics-workbench/resources/test-selection-guide.md +93 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.R +78 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.py +210 -0
- package/skills/camera-trap-processing/SKILL.md +159 -0
- package/skills/camera-trap-processing/examples/example-prompts.md +103 -0
- package/skills/camera-trap-processing/examples/leopard_serengeti_example.md +231 -0
- package/skills/camera-trap-processing/resources/activity-patterns-reference.md +113 -0
- package/skills/camera-trap-processing/resources/camtrapR-workflow-guide.md +130 -0
- package/skills/camera-trap-processing/resources/detection-event-definition-guide.md +89 -0
- package/skills/camera-trap-processing/scripts/estimate_activity.R +169 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.R +179 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.py +192 -0
- package/skills/community-ecology-ordination/SKILL.md +133 -0
- package/skills/community-ecology-ordination/examples/example-prompts.md +35 -0
- package/skills/community-ecology-ordination/resources/dissimilarity-metric-guide.md +53 -0
- package/skills/community-ecology-ordination/resources/nmds-interpretation-guide.md +104 -0
- package/skills/community-ecology-ordination/scripts/__pycache__/community_analysis.cpython-311.pyc +0 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.R +143 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.py +231 -0
- package/skills/ecological-data-foundation/SKILL.md +129 -0
- package/skills/ecological-data-foundation/examples/example-prompts.md +40 -0
- package/skills/ecological-data-foundation/resources/coordinate-cleaning-flags.md +66 -0
- package/skills/ecological-data-foundation/resources/darwin-core-glossary.md +91 -0
- package/skills/ecological-data-foundation/resources/data-citation-guide.md +265 -0
- package/skills/ecological-data-foundation/resources/gbif-data-citation-guide.md +193 -0
- package/skills/ecological-data-foundation/resources/qa-checklist.md +83 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/clean_occurrences.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_ebird.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_inat.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_iucn.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_obis.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.R +230 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.py +268 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.R +251 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.py +364 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.R +315 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.py +407 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.R +238 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.py +304 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.R +273 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.py +344 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.R +248 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.py +318 -0
- package/skills/ecological-impact-assessment/SKILL.md +123 -0
- package/skills/ecological-impact-assessment/examples/example-prompts.md +32 -0
- package/skills/ecological-impact-assessment/resources/baci-design-guide.md +55 -0
- package/skills/ecological-impact-assessment/resources/fragmentation-metrics-reference.md +86 -0
- package/skills/ecological-impact-assessment/resources/pressure-index-template.md +78 -0
- package/skills/ecological-impact-assessment/resources/study-design-guide.md +168 -0
- package/skills/ecological-impact-assessment/scripts/baci_analysis.R +161 -0
- package/skills/ecological-impact-assessment/scripts/fragmentation_analysis.py +141 -0
- package/skills/ecological-impact-assessment/scripts/power_analysis_baci.R +274 -0
- package/skills/ecosystem-services-assessment/SKILL.md +125 -0
- package/skills/ecosystem-services-assessment/examples/example-prompts.md +24 -0
- package/skills/ecosystem-services-assessment/resources/es-indicator-reference.md +45 -0
- package/skills/ecosystem-services-assessment/resources/invest-parameter-guide.md +86 -0
- package/skills/ecosystem-services-assessment/resources/rusle-coefficients.md +88 -0
- package/skills/ecosystem-services-assessment/scripts/__pycache__/compute_es.cpython-311.pyc +0 -0
- package/skills/ecosystem-services-assessment/scripts/compute_es.py +189 -0
- package/skills/ecosystem-services-assessment/scripts/tradeoff_analysis.R +161 -0
- package/skills/environmental-time-series/SKILL.md +125 -0
- package/skills/environmental-time-series/examples/example-prompts.md +33 -0
- package/skills/environmental-time-series/resources/anomaly-indices-reference.md +88 -0
- package/skills/environmental-time-series/resources/bfast-parameter-guide.md +69 -0
- package/skills/environmental-time-series/scripts/__pycache__/recovery_trajectory.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/__pycache__/trend_analysis.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.R +305 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.py +178 -0
- package/skills/environmental-time-series/scripts/trend_analysis.R +192 -0
- package/skills/environmental-time-series/scripts/trend_analysis.py +184 -0
- package/skills/geoprocessing-for-ecology/SKILL.md +123 -0
- package/skills/geoprocessing-for-ecology/examples/example-prompts.md +32 -0
- package/skills/geoprocessing-for-ecology/resources/crs-reference.md +62 -0
- package/skills/geoprocessing-for-ecology/resources/global-predictor-sources.md +331 -0
- package/skills/geoprocessing-for-ecology/resources/resampling-methods.md +57 -0
- package/skills/geoprocessing-for-ecology/scripts/__pycache__/download_predictors.cpython-311.pyc +0 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.R +239 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.py +379 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.R +224 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.py +172 -0
- package/skills/landscape-connectivity/SKILL.md +170 -0
- package/skills/landscape-connectivity/examples/example-prompts.md +96 -0
- package/skills/landscape-connectivity/examples/jaguar_mesoamerica_corridor_example.md +271 -0
- package/skills/landscape-connectivity/resources/circuitscape-parameter-guide.md +155 -0
- package/skills/landscape-connectivity/resources/graph-theory-for-ecology.md +134 -0
- package/skills/landscape-connectivity/resources/resistance-surface-guide.md +141 -0
- package/skills/landscape-connectivity/scripts/connectivity_analysis.py +387 -0
- package/skills/landscape-connectivity/scripts/connectivity_metrics.R +274 -0
- package/skills/landscape-connectivity/scripts/resistance_surface.R +239 -0
- package/skills/model-validation-and-uncertainty/SKILL.md +131 -0
- package/skills/model-validation-and-uncertainty/examples/example-prompts.md +30 -0
- package/skills/model-validation-and-uncertainty/resources/extrapolation-risk-guide.md +236 -0
- package/skills/model-validation-and-uncertainty/resources/metric-selection-guide.md +52 -0
- package/skills/model-validation-and-uncertainty/resources/threshold-selection-guide.md +64 -0
- package/skills/model-validation-and-uncertainty/scripts/__pycache__/validate_model.cpython-311.pyc +0 -0
- package/skills/model-validation-and-uncertainty/scripts/extrapolation_risk.R +315 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_model.py +226 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_sdm.R +162 -0
- package/skills/occupancy-and-detection/SKILL.md +126 -0
- package/skills/occupancy-and-detection/examples/example-prompts.md +33 -0
- package/skills/occupancy-and-detection/resources/detection-history-format.md +100 -0
- package/skills/occupancy-and-detection/resources/occupancy-study-design.md +47 -0
- package/skills/occupancy-and-detection/scripts/__pycache__/occupancy_analysis.cpython-311.pyc +0 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.R +160 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.py +159 -0
- package/skills/population-viability-analysis/SKILL.md +161 -0
- package/skills/population-viability-analysis/examples/african_elephant_pva_example.md +266 -0
- package/skills/population-viability-analysis/examples/example-prompts.md +95 -0
- package/skills/population-viability-analysis/resources/extinction-risk-thresholds.md +128 -0
- package/skills/population-viability-analysis/resources/matrix-model-guide.md +139 -0
- package/skills/population-viability-analysis/resources/sensitivity-elasticity-reference.md +182 -0
- package/skills/population-viability-analysis/scripts/matrix_pva.R +258 -0
- package/skills/population-viability-analysis/scripts/pva_analysis.py +442 -0
- package/skills/population-viability-analysis/scripts/stochastic_pva.R +353 -0
- package/skills/predictive-modeling-best-practices/SKILL.md +136 -0
- package/skills/predictive-modeling-best-practices/examples/example-prompts.md +58 -0
- package/skills/predictive-modeling-best-practices/resources/collinearity-decision-tree.md +65 -0
- package/skills/predictive-modeling-best-practices/resources/sampling-bias-correction.md +267 -0
- package/skills/predictive-modeling-best-practices/resources/spatial-cv-guide.md +73 -0
- package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc +0 -0
- package/skills/predictive-modeling-best-practices/scripts/collinearity_check.R +112 -0
- package/skills/predictive-modeling-best-practices/scripts/spatial_cv.py +182 -0
- package/skills/reproducible-ecology-pipeline/SKILL.md +139 -0
- package/skills/reproducible-ecology-pipeline/examples/example-prompts.md +35 -0
- package/skills/reproducible-ecology-pipeline/resources/directory-structure-template.md +94 -0
- package/skills/reproducible-ecology-pipeline/resources/params-yaml-template.yaml +84 -0
- package/skills/reproducible-ecology-pipeline/resources/reproducibility-checklist-template.md +66 -0
- package/skills/reproducible-ecology-pipeline/scripts/generate_file_manifest.py +110 -0
- package/skills/reproducible-ecology-pipeline/scripts/init_project.sh +53 -0
- package/skills/spatial-prioritization/SKILL.md +162 -0
- package/skills/spatial-prioritization/examples/biodiversity_hotspot_prioritization_example.md +289 -0
- package/skills/spatial-prioritization/examples/example-prompts.md +93 -0
- package/skills/spatial-prioritization/resources/cost-surface-reference.md +130 -0
- package/skills/spatial-prioritization/resources/marxan-vs-prioritizr-comparison.md +125 -0
- package/skills/spatial-prioritization/resources/prioritizr-formulation-guide.md +188 -0
- package/skills/spatial-prioritization/resources/representation-targets-guide.md +186 -0
- package/skills/spatial-prioritization/scripts/prioritization_sensitivity.R +320 -0
- package/skills/spatial-prioritization/scripts/run_prioritization.R +336 -0
- package/skills/species-distribution-modeling/SKILL.md +139 -0
- package/skills/species-distribution-modeling/examples/example-prompts.md +36 -0
- package/skills/species-distribution-modeling/resources/algorithm-comparison.md +25 -0
- package/skills/species-distribution-modeling/resources/calibration-area-guide.md +71 -0
- package/skills/species-distribution-modeling/resources/climate-scenario-preparation.md +170 -0
- package/skills/species-distribution-modeling/resources/maxent-calibration-guide.md +211 -0
- package/skills/species-distribution-modeling/resources/sdm-checklist.md +37 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.R +236 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.py +286 -0
- package/skills/species-distribution-modeling/scripts/prepare_future_layers.R +351 -0
- package/skills/species-distribution-modeling/scripts/project_scenarios.R +220 -0
- package/skills/species-distribution-modeling/scripts/run_ensemble_sdm.R +99 -0
- package/skills/species-distribution-modeling/scripts/sdm_pipeline.py +318 -0
- package/skills/species-distribution-modeling/scripts/tune_maxnet.R +344 -0
- package/templates/SKILL_TEMPLATE.md +225 -0
- package/templates/checklists/data-submission-checklist.md +38 -0
- package/templates/checklists/post-analysis-checklist.md +55 -0
- package/templates/checklists/pre-analysis-checklist.md +31 -0
- package/templates/prompts/debug-skill.md +47 -0
- package/templates/prompts/invoke-skill.md +34 -0
- package/templates/prompts/invoke-workflow.md +45 -0
- package/templates/reports/technical-report-template.md +80 -0
- package/templates/scripts/logger_setup.R +79 -0
- package/templates/scripts/logger_setup.py +119 -0
- package/templates/scripts/params_loader.R +28 -0
- package/templates/scripts/params_loader.py +38 -0
- package/workflows/analyze-community-structure/WORKFLOW.md +72 -0
- package/workflows/analyze-environmental-change/WORKFLOW.md +73 -0
- package/workflows/assess-ecological-impact/WORKFLOW.md +75 -0
- package/workflows/assess-ecosystem-services/WORKFLOW.md +68 -0
- package/workflows/assess-landscape-connectivity/WORKFLOW.md +84 -0
- package/workflows/build-fire-risk-map/WORKFLOW.md +79 -0
- package/workflows/produce-technical-report/WORKFLOW.md +113 -0
- package/workflows/run-camera-trap-occupancy/WORKFLOW.md +87 -0
- package/workflows/run-conservation-prioritization/WORKFLOW.md +89 -0
- package/workflows/run-multispecies-screening/WORKFLOW.md +197 -0
- package/workflows/run-occupancy-analysis/WORKFLOW.md +74 -0
- package/workflows/run-population-viability/WORKFLOW.md +90 -0
- package/workflows/run-sdm-study/WORKFLOW.md +99 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Sampling Bias Correction Guide
|
|
2
|
+
|
|
3
|
+
Detecting and correcting geographic sampling bias in occurrence records before SDM fitting.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Why Sampling Bias Distorts SDMs
|
|
8
|
+
|
|
9
|
+
Occurrence data from aggregated databases (GBIF, VertNet, iNaturalist) rarely represent
|
|
10
|
+
true species distributions. They reflect **where people search**, not where species occur.
|
|
11
|
+
|
|
12
|
+
### Sources of geographic bias
|
|
13
|
+
|
|
14
|
+
| Bias source | Mechanism | Effect on SDM |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| **Road/trail proximity** | Collectors follow accessible paths | Roadsides over-represented in environmental space |
|
|
17
|
+
| **Urban proximity** | Amateur naturalists concentrate near cities | Urban bioclimates over-represented |
|
|
18
|
+
| **Research institutions** | Field stations generate intense local records | Hyper-local clusters in training data |
|
|
19
|
+
| **National boundaries** | Data sharing policies differ by country | Abrupt density gradients at borders |
|
|
20
|
+
| **Language barriers** | Non-English-speaking regions under-sampled in GBIF | Geographic gaps unrelated to species ecology |
|
|
21
|
+
|
|
22
|
+
### Consequence for model fitting
|
|
23
|
+
|
|
24
|
+
When occurrence records are biased toward roadsides, MaxEnt or BRT will learn that
|
|
25
|
+
road-adjacent environments predict presence. Background points sampled randomly will
|
|
26
|
+
under-represent those environments. The model will incorrectly associate road proximity
|
|
27
|
+
with habitat suitability.
|
|
28
|
+
|
|
29
|
+
**Key reference:** Phillips et al. 2009. Sample selection bias and presence-only distribution models.
|
|
30
|
+
*Ecological Applications* 19: 181–197. DOI: [10.1890/07-2153.1](https://doi.org/10.1890/07-2153.1)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Detecting Sampling Bias
|
|
35
|
+
|
|
36
|
+
### Spatial thinning (standard — already in `ecological-data-foundation`)
|
|
37
|
+
|
|
38
|
+
Remove records closer than a specified distance to reduce spatial clustering:
|
|
39
|
+
|
|
40
|
+
```r
|
|
41
|
+
suppressPackageStartupMessages(library(spThin))
|
|
42
|
+
thinned <- thin(loc.data = occ_df,
|
|
43
|
+
lat.col = "decimalLatitude",
|
|
44
|
+
long.col = "decimalLongitude",
|
|
45
|
+
spec.col = "species",
|
|
46
|
+
thin.par = 10, # minimum distance in km
|
|
47
|
+
reps = 10,
|
|
48
|
+
locs.thinned.list.return = TRUE,
|
|
49
|
+
write.files = FALSE)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Kernel density bias map
|
|
53
|
+
|
|
54
|
+
Visualise sampling density to diagnose the pattern of bias:
|
|
55
|
+
|
|
56
|
+
```r
|
|
57
|
+
suppressPackageStartupMessages(library(MASS))
|
|
58
|
+
suppressPackageStartupMessages(library(terra))
|
|
59
|
+
|
|
60
|
+
# Estimate 2D kernel density of occurrences
|
|
61
|
+
kde <- kde2d(occ_df$decimalLongitude, occ_df$decimalLatitude,
|
|
62
|
+
n = 200,
|
|
63
|
+
lims = c(range(occ_df$decimalLongitude) + c(-2, 2),
|
|
64
|
+
range(occ_df$decimalLatitude) + c(-2, 2)))
|
|
65
|
+
|
|
66
|
+
# Convert to SpatRaster
|
|
67
|
+
bias_rast <- rast(list(x = kde$x, y = kde$y, z = kde$z))
|
|
68
|
+
plot(bias_rast, main = "Sampling density kernel (darker = more sampled)")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Environmental distribution comparison
|
|
72
|
+
|
|
73
|
+
Compare environmental space of occurrences vs. background to detect bias:
|
|
74
|
+
|
|
75
|
+
```r
|
|
76
|
+
# Extract env values at occurrence and background points
|
|
77
|
+
occ_env <- extract(env_stack, occ_pts)
|
|
78
|
+
bg_env <- extract(env_stack, bg_pts)
|
|
79
|
+
|
|
80
|
+
# Kolmogorov-Smirnov test per variable
|
|
81
|
+
for (v in names(env_stack)) {
|
|
82
|
+
ks_result <- ks.test(occ_env[[v]], bg_env[[v]])
|
|
83
|
+
cat(v, ": D =", round(ks_result$statistic, 3),
|
|
84
|
+
"p =", round(ks_result$p.value, 4), "\n")
|
|
85
|
+
}
|
|
86
|
+
# Significant D (p < 0.05) suggests occurrence records do NOT represent
|
|
87
|
+
# the available environmental space — evidence of sampling bias
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 3. Correction Methods
|
|
93
|
+
|
|
94
|
+
### Method 1 — Target-Group Background
|
|
95
|
+
|
|
96
|
+
**Concept:** Use occurrence records of all species from the same taxonomic group (e.g.,
|
|
97
|
+
all mammals, all birds) as the background. This approximates the sampling effort:
|
|
98
|
+
if a site was visited (evidenced by other species records), it should appear in the background.
|
|
99
|
+
|
|
100
|
+
**When to use:**
|
|
101
|
+
- When sampling effort tracks detectability (organised surveys, citizen science platforms)
|
|
102
|
+
- When a large pool of co-occurring taxonomic relatives exists in GBIF
|
|
103
|
+
|
|
104
|
+
**Limitations:**
|
|
105
|
+
- Assumes all species in the group have similar detectability
|
|
106
|
+
- Fails if the focal species was specifically targeted (e.g., camera trap targeted at jaguars)
|
|
107
|
+
|
|
108
|
+
```r
|
|
109
|
+
suppressPackageStartupMessages(library(rgbif))
|
|
110
|
+
|
|
111
|
+
# Download all mammal records in the study region as target-group background
|
|
112
|
+
tg_bg <- occ_search(
|
|
113
|
+
orderKey = 732, # Carnivora taxon key
|
|
114
|
+
hasCoordinate = TRUE,
|
|
115
|
+
occurrenceStatus = "PRESENT",
|
|
116
|
+
country = "BR",
|
|
117
|
+
limit = 50000
|
|
118
|
+
)$data
|
|
119
|
+
|
|
120
|
+
# Remove focal species from background
|
|
121
|
+
tg_bg <- tg_bg[tg_bg$species != "Panthera onca", ]
|
|
122
|
+
|
|
123
|
+
bg_pts <- tg_bg[, c("decimalLongitude", "decimalLatitude")]
|
|
124
|
+
bg_pts <- na.omit(bg_pts)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Report in ODMAP field O4:** "Background sampled from target-group (Carnivora) GBIF records (n = X) to account for collector bias."
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
### Method 2 — Kernel Density Weighting
|
|
132
|
+
|
|
133
|
+
**Concept:** Weight background points inversely proportional to sampling density.
|
|
134
|
+
Areas that are densely sampled get low weight; areas rarely visited get high weight.
|
|
135
|
+
|
|
136
|
+
**Formula:**
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
weight(bg_i) = 1 / KDE(bg_i)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
where KDE is the kernel density estimate of occurrence records at the background point location.
|
|
143
|
+
|
|
144
|
+
```r
|
|
145
|
+
suppressPackageStartupMessages(library(MASS))
|
|
146
|
+
suppressPackageStartupMessages(library(terra))
|
|
147
|
+
|
|
148
|
+
# Compute KDE of occurrence points
|
|
149
|
+
kde <- kde2d(occ_df$decimalLongitude, occ_df$decimalLatitude,
|
|
150
|
+
n = 200,
|
|
151
|
+
lims = c(range(occ_df$decimalLongitude) + c(-5, 5),
|
|
152
|
+
range(occ_df$decimalLatitude) + c(-5, 5)))
|
|
153
|
+
|
|
154
|
+
# Interpolate KDE values at background point locations
|
|
155
|
+
bias_rast <- rast(list(x = kde$x, y = kde$y, z = kde$z))
|
|
156
|
+
bg_density <- extract(bias_rast, bg_pts)[[1]]
|
|
157
|
+
|
|
158
|
+
# Invert density to get weights (add small constant to avoid division by zero)
|
|
159
|
+
bg_weights <- 1 / (bg_density + 1e-6)
|
|
160
|
+
bg_weights <- bg_weights / sum(bg_weights, na.rm = TRUE)
|
|
161
|
+
|
|
162
|
+
# Pass bg_weights to maxnet or ENMeval via:
|
|
163
|
+
# ENMevaluate(..., bg = bg_pts, bg.grp = NULL)
|
|
164
|
+
# maxnet::maxnet(p, data, bg.weights = bg_weights)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### Method 3 — Environmental Filtering (thin in environmental space)
|
|
170
|
+
|
|
171
|
+
**Concept:** Instead of geographic thinning (by distance), thin occurrence records in
|
|
172
|
+
environmental space to reduce over-representation of common bioclimates.
|
|
173
|
+
|
|
174
|
+
**When to use:**
|
|
175
|
+
- When geographic thinning removes records in distinct habitats that happen to be close
|
|
176
|
+
- When most records cluster in one bioclimatic region
|
|
177
|
+
|
|
178
|
+
```r
|
|
179
|
+
suppressPackageStartupMessages(library(terra))
|
|
180
|
+
|
|
181
|
+
# Extract env values at occurrences
|
|
182
|
+
occ_env <- extract(env_stack, occ_pts, ID = FALSE)
|
|
183
|
+
|
|
184
|
+
# Create a raster in environmental space (first two PCA axes)
|
|
185
|
+
pca_res <- prcomp(na.omit(occ_env), scale. = TRUE)
|
|
186
|
+
occ_pca <- predict(pca_res, occ_env)[, 1:2]
|
|
187
|
+
|
|
188
|
+
# Grid sampling in environmental space (keep 1 record per cell)
|
|
189
|
+
env_grid_size <- 0.5 # in PC1/PC2 units — adjust based on variance explained
|
|
190
|
+
|
|
191
|
+
occ_pca_df <- as.data.frame(occ_pca)
|
|
192
|
+
occ_pca_df$cell_id <- paste(
|
|
193
|
+
floor(occ_pca_df$PC1 / env_grid_size),
|
|
194
|
+
floor(occ_pca_df$PC2 / env_grid_size)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Keep one record per environmental cell (random)
|
|
198
|
+
set.seed(42)
|
|
199
|
+
occ_thinned_idx <- occ_pca_df %>%
|
|
200
|
+
dplyr::group_by(cell_id) %>%
|
|
201
|
+
dplyr::slice_sample(n = 1) %>%
|
|
202
|
+
dplyr::pull(.I)
|
|
203
|
+
|
|
204
|
+
occ_thinned <- occ_df[occ_thinned_idx, ]
|
|
205
|
+
message("Environmental thinning: ", nrow(occ_df), " → ", nrow(occ_thinned), " records")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### Method 4 — Checkerboard / Spatial Partitioning
|
|
211
|
+
|
|
212
|
+
See `resources/spatial-cv-guide.md` for detailed implementation. Spatial partitioning
|
|
213
|
+
(checkerboard or block CV) does not correct bias but ensures that validation data are
|
|
214
|
+
geographically independent from training data, making model evaluation more realistic
|
|
215
|
+
under spatially biased training.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. Decision Table — Which Method to Use
|
|
220
|
+
|
|
221
|
+
| Type of bias | Data available | Recommended method |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| Road/city proximity, known from KDE | Target-group records in GBIF | Target-group background |
|
|
224
|
+
| General sampling density gradient | Any | Kernel density weighting |
|
|
225
|
+
| Bioclimatic clustering (few env types over-sampled) | Environmental predictors | Environmental filtering |
|
|
226
|
+
| All of the above | Any | Combine: KDE weighting + environmental filtering |
|
|
227
|
+
| Bias unknown but suspicious | Any | Spatial thinning (1 record per grid cell) as minimum |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## 5. Reporting in ODMAP (Field O4)
|
|
232
|
+
|
|
233
|
+
ODMAP field **O4 (Biases and sampling artefacts)** must state:
|
|
234
|
+
1. Which bias was detected and how (KDE, KS test, visual inspection)
|
|
235
|
+
2. Which correction method was applied
|
|
236
|
+
3. Key parameters (target group taxon, KDE bandwidth, grid cell size)
|
|
237
|
+
|
|
238
|
+
**Example ODMAP O4 text:**
|
|
239
|
+
> "Sampling bias was detected by comparing the environmental distribution of occurrence
|
|
240
|
+
> records vs. random background (KS test, p < 0.001 for bio1 and bio12). Bias was
|
|
241
|
+
> corrected using target-group background (all Mammalia records from GBIF within the
|
|
242
|
+
> study area, n = 12,450), excluding the focal species."
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 6. Common Pitfalls
|
|
247
|
+
|
|
248
|
+
- **Spatial thinning alone is not bias correction:** it reduces spatial clustering but
|
|
249
|
+
does not address the underlying pattern of collector effort.
|
|
250
|
+
- **KDE bandwidth selection is subjective:** use cross-validated bandwidth (`MASS::bandwidth.nrd`
|
|
251
|
+
or `ks::hpi`) rather than default.
|
|
252
|
+
- **Target-group background can introduce new bias:** if all Carnivora records are
|
|
253
|
+
also biased toward roads, the background will still be biased. Always visualise.
|
|
254
|
+
- **Environmental filtering removes rare habitats:** if a unique habitat is represented
|
|
255
|
+
by only a few records, environmental filtering will preferentially discard it. Check
|
|
256
|
+
that important biomes are still represented after filtering.
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## 7. References
|
|
261
|
+
|
|
262
|
+
| Citation | DOI |
|
|
263
|
+
|---|---|
|
|
264
|
+
| Phillips et al. 2009. Ecol. Apps. 19:181–197 | [10.1890/07-2153.1](https://doi.org/10.1890/07-2153.1) |
|
|
265
|
+
| Fourcade et al. 2014. PLoS ONE 9:e97122 | [10.1371/journal.pone.0097122](https://doi.org/10.1371/journal.pone.0097122) |
|
|
266
|
+
| Kramer-Schadt et al. 2013. Ecography 36:1044 | [10.1111/j.1600-0587.2013.00159.x](https://doi.org/10.1111/j.1600-0587.2013.00159.x) |
|
|
267
|
+
| Warton & Shepherd 2010. Ann. Appl. Stat. | [10.1214/10-AOAS331](https://doi.org/10.1214/10-AOAS331) |
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Spatial Cross-Validation Guide
|
|
2
|
+
|
|
3
|
+
## Why Spatial CV?
|
|
4
|
+
|
|
5
|
+
Standard random k-fold CV assumes independence between train and test folds. Ecological and SDM data are almost always spatially autocorrelated — nearby sites share similar environments and species composition. Random splits leak spatial information across folds, producing optimistically biased performance estimates.
|
|
6
|
+
|
|
7
|
+
**Rule:** If the response variable is spatially structured (which is almost always true for ecological data), use spatial CV.
|
|
8
|
+
|
|
9
|
+
## Block CV (Checkerboard / Grid)
|
|
10
|
+
|
|
11
|
+
Divides the study area into rectangular blocks. Points within the same block are assigned to the same fold.
|
|
12
|
+
|
|
13
|
+
```r
|
|
14
|
+
library(blockCV)
|
|
15
|
+
# Auto-select block size based on spatial autocorrelation range
|
|
16
|
+
sac <- cv_spatial_autocor(
|
|
17
|
+
x = occ_sf, # sf object with presence/background
|
|
18
|
+
column = "response",
|
|
19
|
+
plot = TRUE
|
|
20
|
+
)
|
|
21
|
+
blocks <- cv_spatial(
|
|
22
|
+
x = occ_sf,
|
|
23
|
+
column = "response",
|
|
24
|
+
k = 5,
|
|
25
|
+
size = sac$range, # use autocorrelation range as block size
|
|
26
|
+
hexagon = FALSE,
|
|
27
|
+
report = TRUE,
|
|
28
|
+
plot = TRUE
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Buffered Leave-One-Out (spatial LOO)
|
|
33
|
+
|
|
34
|
+
For each test point, exclude all training points within a buffer distance. Computationally expensive but most rigorous for small datasets.
|
|
35
|
+
|
|
36
|
+
```r
|
|
37
|
+
# In ENMeval:
|
|
38
|
+
library(ENMeval)
|
|
39
|
+
e <- ENMevaluate(
|
|
40
|
+
occs = occ_coords,
|
|
41
|
+
envs = predictor_stack,
|
|
42
|
+
bg = bg_coords,
|
|
43
|
+
algorithm = "maxnet",
|
|
44
|
+
partitions = "block" # or "checkerboard1", "checkerboard2"
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Recommended Block Size
|
|
49
|
+
|
|
50
|
+
The block size should be at least as large as the spatial autocorrelation range of the response variable.
|
|
51
|
+
|
|
52
|
+
| Spatial resolution | Suggested starting block size |
|
|
53
|
+
|--------------------|-------------------------------|
|
|
54
|
+
| 1 km | 50–100 km |
|
|
55
|
+
| 5 km | 100–200 km |
|
|
56
|
+
| 10 km | 200–400 km |
|
|
57
|
+
| 25 km (continental) | 500–1000 km |
|
|
58
|
+
|
|
59
|
+
Always run `cv_spatial_autocor()` to confirm the empirical range for your specific dataset.
|
|
60
|
+
|
|
61
|
+
## Number of Folds
|
|
62
|
+
|
|
63
|
+
- **k = 4 or 5:** Standard. Balances bias and variance.
|
|
64
|
+
- **k = 10:** For larger datasets (> 500 occurrences).
|
|
65
|
+
- **Leave-one-out (LOO):** For very small datasets (< 30 occurrences).
|
|
66
|
+
|
|
67
|
+
## Checklist Before Running CV
|
|
68
|
+
|
|
69
|
+
- [ ] Confirmed spatial autocorrelation in response variable
|
|
70
|
+
- [ ] Block size ≥ autocorrelation range
|
|
71
|
+
- [ ] Each fold has presence AND background points
|
|
72
|
+
- [ ] No fold is empty or severely imbalanced (< 10 presences per fold)
|
|
73
|
+
- [ ] Same CV folds used across all candidate algorithms for fair comparison
|
package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc
ADDED
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
# Usage: Rscript collinearity_check.R <predictors.csv> <output_dir> [vif_threshold] [cor_threshold]
|
|
5
|
+
# Assess and reduce predictor collinearity
|
|
6
|
+
# Usage: Rscript collinearity_check.R <env_matrix_csv> <output_dir> [vif_threshold]
|
|
7
|
+
# Requires: usdm, corrplot, dplyr
|
|
8
|
+
|
|
9
|
+
# ── Inline logger ─────────────────────────────────────────────────────────────
|
|
10
|
+
SKILL_NAME <- "predictive-modeling-best-practices"
|
|
11
|
+
.log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
|
|
12
|
+
log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
|
|
13
|
+
log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
|
|
14
|
+
log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
|
|
15
|
+
log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
|
|
16
|
+
log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
|
|
17
|
+
dir.create("logs", recursive=TRUE, showWarnings=FALSE)
|
|
18
|
+
|
|
19
|
+
suppressPackageStartupMessages({
|
|
20
|
+
library(usdm)
|
|
21
|
+
library(dplyr)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
args <- commandArgs(trailingOnly = TRUE)
|
|
25
|
+
env_file <- ifelse(length(args) >= 1, args[1], "data/processed/env_matrix.csv")
|
|
26
|
+
output_dir <- ifelse(length(args) >= 2, args[2], "outputs")
|
|
27
|
+
vif_threshold <- ifelse(length(args) >= 3, as.numeric(args[3]), 5)
|
|
28
|
+
|
|
29
|
+
# ── Input precondition checks ─────────────────────────────────────────────────
|
|
30
|
+
if (!file.exists(env_file)) {
|
|
31
|
+
log_error("Input nao encontrado: %s\nCausa provavel: passo anterior nao concluiu.\nVerifique: outputs do skill anterior.\nSkill anterior: species-distribution-modeling", env_file)
|
|
32
|
+
stop("Missing input: ", env_file)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log_decision("vif_threshold", vif_threshold, "VIF threshold for stepwise predictor exclusion; standard ecological threshold is 5 or 10")
|
|
36
|
+
|
|
37
|
+
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
|
|
38
|
+
|
|
39
|
+
# ── Load ───────────────────────────────────────────────────────────────────
|
|
40
|
+
log_step(1, "Load environmental predictor matrix")
|
|
41
|
+
tryCatch({
|
|
42
|
+
log_info("Loading: %s", env_file)
|
|
43
|
+
env <- read.csv(env_file) |> na.omit()
|
|
44
|
+
log_info("Variables: %d | Rows: %d", ncol(env), nrow(env))
|
|
45
|
+
|
|
46
|
+
if (nrow(env) < 30) {
|
|
47
|
+
log_warn("Numero de linhas baixo (%d). Estimativas de correlacao podem ser instáveis com n < 30.", nrow(env))
|
|
48
|
+
}
|
|
49
|
+
if (ncol(env) < 2) {
|
|
50
|
+
log_error("Apenas %d variavel encontrada. Analise de colinearidade requer pelo menos 2 preditores.\nCausa provavel: CSV incorreto ou sem preditores numericos.\nVerifique: formato do arquivo env_matrix_csv.\nSkill anterior: species-distribution-modeling", ncol(env))
|
|
51
|
+
stop("At least 2 predictor columns required for collinearity analysis.")
|
|
52
|
+
}
|
|
53
|
+
}, error = function(e) {
|
|
54
|
+
log_error("Falha em load_env_matrix: %s\nCausa provavel: arquivo CSV ausente, malformado ou sem colunas numericas.\nVerifique: caminho e formato do CSV de preditores.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
|
|
55
|
+
stop(e)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# ── Pairwise correlation ───────────────────────────────────────────────────
|
|
59
|
+
log_step(2, "Compute pairwise Pearson correlations")
|
|
60
|
+
tryCatch({
|
|
61
|
+
cor_mat <- cor(env, method = "pearson")
|
|
62
|
+
high_cor <- which(abs(cor_mat) > 0.7 & cor_mat != 1, arr.ind = TRUE)
|
|
63
|
+
high_cor_pairs <- data.frame(
|
|
64
|
+
var1 = rownames(high_cor),
|
|
65
|
+
var2 = colnames(cor_mat)[high_cor[, 2]],
|
|
66
|
+
r = cor_mat[high_cor]
|
|
67
|
+
) |> filter(var1 < var2) |> arrange(desc(abs(r)))
|
|
68
|
+
|
|
69
|
+
log_info("Highly correlated pairs (|r| > 0.7): %d pairs found.", nrow(high_cor_pairs))
|
|
70
|
+
if (nrow(high_cor_pairs) > 0) {
|
|
71
|
+
log_warn("%d pares de preditores altamente correlacionados (|r| > 0.70) detectados. Reducao de colinearidade necessaria.", nrow(high_cor_pairs))
|
|
72
|
+
log_info("Highly correlated pairs:\n%s",
|
|
73
|
+
paste(capture.output(print(high_cor_pairs)), collapse = "\n"))
|
|
74
|
+
}
|
|
75
|
+
}, error = function(e) {
|
|
76
|
+
log_error("Falha em pairwise_correlation: %s\nCausa provavel: colunas nao numericas ou valores NA remanescentes.\nVerifique: tipos de dados do CSV e resultado do na.omit.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
|
|
77
|
+
stop(e)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
# ── VIF stepwise reduction ─────────────────────────────────────────────────
|
|
81
|
+
log_step(3, "VIF stepwise predictor reduction")
|
|
82
|
+
tryCatch({
|
|
83
|
+
log_info("Running VIF stepwise reduction (threshold: %g)...", vif_threshold)
|
|
84
|
+
vif_result <- vifstep(env, th = vif_threshold)
|
|
85
|
+
log_info("Variables retained after VIF reduction:\n%s",
|
|
86
|
+
paste(capture.output(print(vif_result)), collapse = "\n"))
|
|
87
|
+
|
|
88
|
+
selected <- vif_result@results$Variables
|
|
89
|
+
log_info("Final selected predictors (%d): %s", length(selected), paste(selected, collapse = ", "))
|
|
90
|
+
log_decision("selected_predictors", paste(selected, collapse = ", "),
|
|
91
|
+
paste0("VIF stepwise retained these predictors below threshold ", vif_threshold))
|
|
92
|
+
|
|
93
|
+
n_removed <- ncol(env) - length(selected)
|
|
94
|
+
if (n_removed > 0) {
|
|
95
|
+
log_warn("%d preditores removidos por VIF > %g. Revise se variaveis ecologicamente importantes foram excluidas.", n_removed, vif_threshold)
|
|
96
|
+
}
|
|
97
|
+
}, error = function(e) {
|
|
98
|
+
log_error("Falha em vif_stepwise_reduction: %s\nCausa provavel: matriz singular, preditores constantes, ou falha no pacote usdm.\nVerifique: variancia de cada preditor e instalacao do pacote usdm.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
|
|
99
|
+
stop(e)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
# ── Outputs ────────────────────────────────────────────────────────────────
|
|
103
|
+
log_step(4, "Write collinearity outputs")
|
|
104
|
+
tryCatch({
|
|
105
|
+
write.csv(high_cor_pairs, file.path(output_dir, "high_correlation_pairs.csv"), row.names = FALSE)
|
|
106
|
+
write.csv(vif_result@results, file.path(output_dir, "vif_results.csv"), row.names = FALSE)
|
|
107
|
+
writeLines(selected, file.path(output_dir, "selected_predictors.txt"))
|
|
108
|
+
log_info("Outputs written to: %s", output_dir)
|
|
109
|
+
}, error = function(e) {
|
|
110
|
+
log_error("Falha em write_outputs: %s\nCausa provavel: permissoes de escrita ou diretorio de saida inexistente.\nVerifique: output_dir e permissoes do sistema de arquivos.\nSkill anterior: species-distribution-modeling", conditionMessage(e))
|
|
111
|
+
stop(e)
|
|
112
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
spatial_cv.py
|
|
7
|
+
Spatial block cross-validation for ecological models.
|
|
8
|
+
Usage: python spatial_cv.py <points_with_env_csv> <output_dir> [n_folds] [block_size_km]
|
|
9
|
+
Requires: pandas, numpy, sklearn, geopandas, matplotlib
|
|
10
|
+
"""
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
SKILL_NAME = "predictive-modeling-best-practices"
|
|
17
|
+
_LOG_DIR = Path("logs")
|
|
18
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
_log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
|
|
23
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
24
|
+
handlers=[
|
|
25
|
+
logging.StreamHandler(sys.stdout),
|
|
26
|
+
logging.FileHandler(_log_file, encoding="utf-8"),
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
logger = logging.getLogger(SKILL_NAME)
|
|
30
|
+
|
|
31
|
+
def log_step(n: int, desc: str) -> None:
|
|
32
|
+
logger.info("-- STEP %d: %s", n, desc)
|
|
33
|
+
|
|
34
|
+
def log_decision(var: str, val, why: str) -> None:
|
|
35
|
+
logger.info("DECISION | %s = %s | %s", var, val, why)
|
|
36
|
+
|
|
37
|
+
import numpy as np
|
|
38
|
+
import pandas as pd
|
|
39
|
+
import matplotlib.pyplot as plt
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assign_spatial_blocks(df, lon_col, lat_col, block_size_deg, n_folds):
|
|
43
|
+
"""Assign spatial blocks by grid, then allocate to CV folds."""
|
|
44
|
+
lon_blocks = np.floor(df[lon_col] / block_size_deg).astype(int)
|
|
45
|
+
lat_blocks = np.floor(df[lat_col] / block_size_deg).astype(int)
|
|
46
|
+
block_ids = (lon_blocks.astype(str) + "_" + lat_blocks.astype(str))
|
|
47
|
+
unique_blocks = block_ids.unique()
|
|
48
|
+
np.random.shuffle(unique_blocks)
|
|
49
|
+
fold_map = {blk: (i % n_folds) + 1 for i, blk in enumerate(unique_blocks)}
|
|
50
|
+
return block_ids.map(fold_map)
|
|
51
|
+
|
|
52
|
+
def collinearity_report(df: pd.DataFrame, predictors: list, r_thresh=0.7) -> pd.DataFrame:
|
|
53
|
+
cor = df[predictors].corr(method="spearman").abs()
|
|
54
|
+
pairs = []
|
|
55
|
+
for i in range(len(predictors)):
|
|
56
|
+
for j in range(i+1, len(predictors)):
|
|
57
|
+
r = cor.iloc[i, j]
|
|
58
|
+
if r > r_thresh:
|
|
59
|
+
pairs.append({"var1": predictors[i], "var2": predictors[j], "spearman_r": round(r, 4)})
|
|
60
|
+
return pd.DataFrame(pairs).sort_values("spearman_r", ascending=False)
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
data_file = sys.argv[1] if len(sys.argv) > 1 else "data/processed/points_with_env.csv"
|
|
64
|
+
output_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("outputs/cv")
|
|
65
|
+
n_folds = int(sys.argv[3]) if len(sys.argv) > 3 else 5
|
|
66
|
+
block_size_km = float(sys.argv[4]) if len(sys.argv) > 4 else 300.0
|
|
67
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
|
|
69
|
+
log_decision("data_file", data_file,
|
|
70
|
+
"Input CSV of occurrence points with extracted environmental predictors")
|
|
71
|
+
log_decision("n_folds", n_folds,
|
|
72
|
+
"Number of spatial CV folds for model evaluation")
|
|
73
|
+
log_decision("block_size_km", block_size_km,
|
|
74
|
+
"Spatial block size in km; should exceed spatial autocorrelation range")
|
|
75
|
+
log_decision("output_dir", str(output_dir), "Directory for CV fold assignments and plots")
|
|
76
|
+
|
|
77
|
+
if not Path(data_file).exists():
|
|
78
|
+
logger.error(
|
|
79
|
+
"Input nao encontrado: %s\n"
|
|
80
|
+
" Causa provavel: passo anterior nao concluiu.\n"
|
|
81
|
+
" Skill anterior que deveria ter produzido este input: geoprocessing-for-ecology",
|
|
82
|
+
data_file
|
|
83
|
+
)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
log_step(1, "Loading point data with environmental predictors")
|
|
88
|
+
dat = pd.read_csv(data_file)
|
|
89
|
+
logger.info("Loaded %d records with %d columns", len(dat), len(dat.columns))
|
|
90
|
+
|
|
91
|
+
lon_col = next((c for c in dat.columns if "lon" in c.lower()), None)
|
|
92
|
+
lat_col = next((c for c in dat.columns if "lat" in c.lower()), None)
|
|
93
|
+
if not lon_col or not lat_col:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"Cannot find lon/lat columns. Name them decimalLongitude/decimalLatitude."
|
|
96
|
+
)
|
|
97
|
+
logger.info("Coordinate columns identified: lon='%s', lat='%s'", lon_col, lat_col)
|
|
98
|
+
|
|
99
|
+
n_missing_coords = dat[[lon_col, lat_col]].isna().any(axis=1).sum()
|
|
100
|
+
if n_missing_coords > 0:
|
|
101
|
+
logger.warning(
|
|
102
|
+
"%d records have missing coordinates and will produce NaN fold assignments.",
|
|
103
|
+
n_missing_coords
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
log_step(2, "Assigning spatial blocks and CV fold labels")
|
|
107
|
+
block_size_deg = block_size_km / 111.0 # approx degrees
|
|
108
|
+
log_decision("block_size_deg", round(block_size_deg, 4),
|
|
109
|
+
"Converted from km using 1 degree ~ 111 km (approximate)")
|
|
110
|
+
np.random.seed(42)
|
|
111
|
+
log_decision("random_seed", 42, "Fixed seed for reproducible fold assignment")
|
|
112
|
+
dat["cv_fold"] = assign_spatial_blocks(dat, lon_col, lat_col, block_size_deg, n_folds)
|
|
113
|
+
|
|
114
|
+
log_step(3, "Summarising fold composition")
|
|
115
|
+
# Fold summary
|
|
116
|
+
fold_summary = dat.groupby("cv_fold").agg(n=("cv_fold","count"))
|
|
117
|
+
if "pa" in dat.columns or "presence" in dat.columns:
|
|
118
|
+
resp_col = "pa" if "pa" in dat.columns else "presence"
|
|
119
|
+
fold_summary["n_presence"] = dat.groupby("cv_fold")[resp_col].sum().values
|
|
120
|
+
|
|
121
|
+
# Check fold balance
|
|
122
|
+
fold_counts = fold_summary["n"].values
|
|
123
|
+
min_fold = fold_counts.min()
|
|
124
|
+
max_fold = fold_counts.max()
|
|
125
|
+
if max_fold > 3 * min_fold:
|
|
126
|
+
logger.warning(
|
|
127
|
+
"Fold sizes are highly imbalanced (min=%d, max=%d). "
|
|
128
|
+
"Consider adjusting block_size_km.",
|
|
129
|
+
min_fold, max_fold
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
logger.info("CV Fold summary (block_size ~%s km):\n%s",
|
|
133
|
+
block_size_km, fold_summary.to_string())
|
|
134
|
+
dat.to_csv(output_dir / "data_with_cv_folds.csv", index=False)
|
|
135
|
+
|
|
136
|
+
log_step(4, "Running collinearity screening on predictors")
|
|
137
|
+
# Collinearity
|
|
138
|
+
skip_cols = {lon_col, lat_col, "pa", "presence", "cv_fold", "QA_status", "species"}
|
|
139
|
+
predictors = [c for c in dat.select_dtypes(include=np.number).columns if c not in skip_cols]
|
|
140
|
+
if predictors:
|
|
141
|
+
cor_pairs = collinearity_report(dat, predictors)
|
|
142
|
+
cor_pairs.to_csv(output_dir / "high_correlation_pairs.csv", index=False)
|
|
143
|
+
logger.info("Highly correlated pairs (|r| > 0.7): %d", len(cor_pairs))
|
|
144
|
+
if len(cor_pairs) > 0:
|
|
145
|
+
logger.warning(
|
|
146
|
+
"%d predictor pairs exceed |r| = 0.7 Spearman correlation threshold. "
|
|
147
|
+
"Consider removing redundant variables before modelling.",
|
|
148
|
+
len(cor_pairs)
|
|
149
|
+
)
|
|
150
|
+
logger.info("%s", cor_pairs.to_string(index=False))
|
|
151
|
+
else:
|
|
152
|
+
logger.info("No highly correlated pairs found.")
|
|
153
|
+
else:
|
|
154
|
+
logger.warning("No numeric predictor columns found for collinearity screening.")
|
|
155
|
+
|
|
156
|
+
log_step(5, "Generating spatial CV fold map plot")
|
|
157
|
+
# Spatial plot
|
|
158
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
159
|
+
scatter = ax.scatter(dat[lon_col], dat[lat_col], c=dat["cv_fold"],
|
|
160
|
+
cmap="Set1", s=15, alpha=0.7)
|
|
161
|
+
plt.colorbar(scatter, ax=ax, label="CV Fold")
|
|
162
|
+
ax.set_xlabel("Longitude"); ax.set_ylabel("Latitude")
|
|
163
|
+
ax.set_title(f"Spatial CV — {n_folds} folds, block ~{block_size_km} km")
|
|
164
|
+
plt.tight_layout()
|
|
165
|
+
plt.savefig(output_dir / "cv_fold_map.png", dpi=150)
|
|
166
|
+
plt.close()
|
|
167
|
+
logger.info("Outputs written to: %s", output_dir)
|
|
168
|
+
|
|
169
|
+
except FileNotFoundError as e:
|
|
170
|
+
logger.error(
|
|
171
|
+
"Input file not found: %s\n"
|
|
172
|
+
" Expected output from: geoprocessing-for-ecology\n"
|
|
173
|
+
" Check that previous step completed.",
|
|
174
|
+
e
|
|
175
|
+
)
|
|
176
|
+
raise
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error("Unexpected error in spatial CV: %s", e)
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|