diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7a9a3ba
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,48 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [develop, main]
+ push:
+ branches: [develop, main]
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ python:
+ name: Python ${{ matrix.python }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ["3.11", "3.13"]
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-python@v7
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ - name: Install
+ run: pip install -e ".[dev]"
+ - name: Lint
+ run: |
+ ruff check src tests
+ ruff format --check src tests
+ - name: Test
+ run: pytest
+
+ site:
+ name: Site
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 22
+ - name: Test
+ run: node --test "tests/web/*.test.mjs"
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..af9c628
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,41 @@
+name: Pages
+
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-python@v7
+ with:
+ python-version: "3.13"
+ - name: Build the analysis package into the site
+ run: pip wheel . --no-deps --wheel-dir docs/package
+ - uses: actions/configure-pages@v6
+ - uses: actions/upload-pages-artifact@v5
+ with:
+ path: docs
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index fa8ae22..e6057c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -319,4 +319,7 @@ data/species/*.geojson
!data/README.md
!results/README.md
!images/README.md
-!docs/README.md
\ No newline at end of file
+!docs/README.md
+# Analysis package built into the site (see `make site` and the Pages workflow)
+docs/package/
+results/*.pkl
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3be61b8..9c5f83a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [Unreleased]
+
+### Added
+- `bee_migration` package under `src/` with `climate`, `analysis`, `prediction`,
+ `evidence`, `charts` and `reports` modules
+- `bee-migration` command (`analyze`, `predict`, `charts`, `all`) with `--data`,
+ `--root` and `--seed` options
+- Test suite under `tests/`
+- Browser dashboard under `docs/`: the package runs in the page through Pyodide
+ and draws trends, correlations, model accuracy and forecasts
+- Field data chapter with observed temperatures (Open-Meteo, ERA5) and the mean
+ latitude of bee records (GBIF), parsed by `bee_migration.sources`
+- `CI` workflow (lint and tests) and `Pages` workflow (site deployment from `main`)
+- Git Flow branching guide in `CONTRIBUTING.md`
+
+### Changed
+- Models are evaluated on the most recent years, held out chronologically, and
+ tuned with time-ordered cross-validation
+- Feature importance is measured by permutation for every model
+- Climate stress index is expressed in regional standard deviations
+- Project metadata, pytest and ruff configuration consolidated in `pyproject.toml`
+
+### Fixed
+- Prediction features no longer include the same-year change of the target
+- Lagged correlations now compare temperature with bee abundance in later years
+- A missing `--data` file is reported as an error instead of silently falling
+ back to simulated data
+- Compatibility with pandas 3
+
+### Removed
+- Trained model files from version control; `bee-migration predict` recreates them
+- Simulated seasonality figures and percentile clipping of observations
+- Generated literature reports; the figures they quoted live in
+ `bee_migration.evidence.literature`
+
## [1.0.0] - 2024-01-21
### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f7771cf..5641b61 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,10 +21,10 @@ This project adheres to a code of conduct that promotes a welcoming and inclusiv
1. Fork the repository
2. Clone your fork: `git clone https://github.com/your-username/Bees.git`
-3. Create a new branch: `git checkout -b feature/your-feature-name`
+3. Create a new branch from `develop`: `git switch -c feature/your-feature-name develop`
4. Make your changes
5. Test your changes
-6. Submit a pull request
+6. Submit a pull request against `develop`
## Development Setup
@@ -88,19 +88,14 @@ pytest tests/test_specific_module.py
- Follow PEP 8 guidelines
- Use meaningful variable and function names
- Add docstrings to all functions and classes
-- Maximum line length: 88 characters (Black formatter)
+- Maximum line length: 88 characters (ruff formatter)
### Code Formatting
```bash
-# Format code with Black
-black src/
-
-# Sort imports with isort
-isort src/
-
-# Lint with flake8
-flake8 src/
+# Format and lint with ruff
+ruff format src/ tests/
+ruff check src/ tests/
```
### Documentation Style
@@ -227,12 +222,36 @@ When requesting features, include:
## Development Workflow
-### Branch Naming
+### Branches
+
+The project follows Git Flow:
+
+| Branch | Purpose | Receives changes from |
+|--------|---------|-----------------------|
+| `main` | Released code; every push deploys the site | `release/*` and `hotfix/*` pull requests |
+| `develop` | Integration of finished work | `feature/*`, `fix/*`, `docs/*`, `refactor/*` pull requests |
+| `feature/description` | New features | branched from `develop` |
+| `fix/description` | Bug fixes | branched from `develop` |
+| `docs/description` | Documentation updates | branched from `develop` |
+| `refactor/description` | Code refactoring | branched from `develop` |
+| `release/x.y.z` | Release preparation | branched from `develop`, merged into `main` and back into `develop` |
+| `hotfix/description` | Urgent fix of released code | branched from `main`, merged into `main` and `develop` |
-- `feature/description`: New features
-- `fix/description`: Bug fixes
-- `docs/description`: Documentation updates
-- `refactor/description`: Code refactoring
+Nothing is pushed directly to `main` or `develop`. Every change arrives through
+a pull request, and the `CI` workflow (lint and tests for Python, tests for the
+site) must pass before it is merged.
+
+```bash
+# Work on something
+git switch -c feature/my-change develop
+# ...commit...
+git push -u origin feature/my-change
+gh pr create --base develop
+
+# Release what is on develop
+git switch -c release/1.1.0 develop
+gh pr create --base main
+```
### Code Review Process
diff --git a/Makefile b/Makefile
index e70c5a2..4882132 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ help:
@echo " test - Run tests"
@echo " test-cov - Run tests with coverage"
@echo " lint - Run linting checks"
- @echo " format - Format code with black and isort"
+ @echo " format - Format code with ruff"
@echo " clean - Clean temporary files"
@echo " run-analysis - Run complete bee migration analysis"
@echo " generate-charts - Generate all scientific charts"
@@ -22,11 +22,11 @@ help:
# Installation
install:
@echo "Installing production dependencies..."
- pip install -r requirements.txt
+ pip install -e .
-install-dev: install
+install-dev:
@echo "Installing development dependencies..."
- pip install -r requirements-dev.txt
+ pip install -e ".[dev]"
# Environment setup
setup-env:
@@ -49,7 +49,7 @@ test:
test-cov:
@echo "Running tests with coverage..."
- pytest tests/ -v --cov=src --cov-report=html --cov-report=term
+ pytest tests/ -v --cov-report=html
test-fast:
@echo "Running fast tests..."
@@ -58,18 +58,16 @@ test-fast:
# Code quality
lint:
@echo "Running linting checks..."
- flake8 src/ tests/
- mypy src/
+ ruff check src/ tests/
format:
@echo "Formatting code..."
- black src/ tests/
- isort src/ tests/
+ ruff format src/ tests/
+ ruff check --fix src/ tests/
format-check:
@echo "Checking code format..."
- black --check src/ tests/
- isort --check-only src/ tests/
+ ruff format --check src/ tests/
# Security
security:
@@ -80,21 +78,25 @@ security:
# Analysis and charts
run-analysis:
@echo "Running complete bee migration analysis..."
- cd src && python bee_analysis.py
- cd src && python bee_migration_analysis.py
- cd src && python comparative_analysis.py
+ bee-migration analyze
generate-charts:
@echo "Generating all scientific charts..."
- cd src && python generate_charts_english.py
-
-generate-charts-pt:
- @echo "Generating charts in Portuguese..."
- cd src && python generate_charts.py
+ bee-migration charts
run-predictor:
@echo "Running migration predictor..."
- cd src && python bee_migration_predictor.py
+ bee-migration predict
+
+# Site
+site:
+ @echo "Building the analysis package into the site and serving it..."
+ pip wheel . --no-deps --wheel-dir docs/package
+ python -m http.server 8000 --bind 127.0.0.1 --directory docs
+
+test-site:
+ @echo "Running site tests..."
+ node --test "tests/web/*.test.mjs"
# Data processing
process-data:
diff --git a/README.md b/README.md
index 579d3c7..f45cc48 100644
--- a/README.md
+++ b/README.md
@@ -37,14 +37,17 @@ This project investigates how climate change affects bee populations in South Am
git clone https://github.com/digomes87/Bees.git
cd Bees
-# Install dependencies
-pip install -r requirements.txt
+# Install the package
+pip install -e .
# Run complete analysis
-python src/bee_analysis.py
+bee-migration analyze
# Run ML predictions
-python src/bee_migration_predictor.py
+bee-migration predict
+
+# Render the charts
+bee-migration charts
```
## 📊 Features
@@ -71,11 +74,15 @@ python src/bee_migration_predictor.py
```
Bees/
-├── src/
-│ ├── bee_analysis.py # Main analysis
-│ ├── bee_migration_predictor.py # ML models
-│ ├── config.py # Configuration
-│ └── generate_charts.py # Visualizations
+├── src/bee_migration/
+│ ├── climate/ # Observations and simulated datasets
+│ ├── analysis/ # Temporal, correlation and geographic patterns
+│ ├── prediction/ # Features, model catalog, training, scores
+│ ├── evidence/ # Figures summarised from the cited studies
+│ ├── charts/ # Chart panels and gallery
+│ ├── reports/ # Plain-text reports
+│ └── cli.py # `bee-migration` command
+├── tests/ # Test suite
├── data/
│ ├── raw/ # Raw data
│ ├── processed/ # Processed data
@@ -147,9 +154,9 @@ Integrated analysis of 6 scientific studies confirms consistent pattern of bee m
- **🔴 Aggressive**: 100% of species migrate by 2040
### ML Performance Metrics
-- **Best Model**: Random Forest (R² = 0.85)
-- **RMSE**: 12.3 (bee abundance)
-- **Important Features**: Temperature, precipitation, latitude
+- **Best Model**: Linear Regression (R² = 0.81 on the four most recent years, held out)
+- **RMSE**: 5.2 (bee abundance)
+- **Data**: simulated observations; see `results/ml_predictions_report.txt`
### Key Insights
1. **Negative Correlation**: Temperature increase reduces abundance
@@ -245,76 +252,69 @@ This project integrated analysis of **6 main scientific studies** on bee migrati
4. **Ecology Letters** - Bumble bee community restructuring (North America)
5. **Kaggle Climate Data** - Climate trend validation
-## 🔧 Configuration
+## 🔧 Usage
-### Environment Variables
-```python
-# config.py
-DATA_SOURCES = {
- 'GBIF_API': 'https://api.gbif.org/v1/',
- 'WORLDCLIM_API': 'https://worldclim.org/data/',
- 'NASA_GIOVANNI': 'https://giovanni.gsfc.nasa.gov/'
-}
-
-ML_CONFIG = {
- 'MODELS': ['RandomForest', 'GradientBoosting', 'SVR'],
- 'CV_FOLDS': 5,
- 'TEST_SIZE': 0.2
-}
-```
+### Command line
-### Analysis Parameters
-- **Period**: 2000-2023
-- **Region**: South America
-- **Species**: 50+ bee species
-- **Resolution**: 1km² (climate data)
+```bash
+bee-migration analyze # trends, correlations, summary report
+bee-migration predict # train and compare the models
+bee-migration charts # render the charts into images/
+bee-migration all # everything above
+
+# Options shared by every command
+bee-migration analyze --data my_observations.csv # use your own data
+bee-migration analyze --root /tmp/run --seed 7 # output folder and random seed
+```
-## 📚 Documentation
+A CSV passed with `--data` needs the columns `year`, `region`, `latitude`,
+`longitude`, `temperature`, `precipitation`, `bee_abundance`,
+`species_diversity` and `honey_production`. Without `--data` the commands run
+on simulated observations (2000-2023, five Brazilian regions).
-### Main Scripts
+### Python
-#### `bee_analysis.py`
```python
-# Complete exploratory analysis
-analyzer = BeeClimateAnalyzer()
-results = analyzer.run_complete_analysis()
-```
+from bee_migration.analysis.correlation import Correlations
+from bee_migration.analysis.temporal import TemporalPatterns
+from bee_migration.climate.period import DEFAULT_PERIOD
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.climate.simulation import ClimateSimulation
+from bee_migration.prediction.training import MigrationPredictor
-#### `bee_migration_predictor.py`
-```python
-# ML predictions
-predictor = BeeMigrationPredictor()
-ml_results = predictor.run_complete_ml_pipeline()
-```
+observations = ClimateSimulation(BRAZILIAN_REGIONS, DEFAULT_PERIOD).observations()
-### Usage Examples
+trends = TemporalPatterns(observations).trends()
+pairs = Correlations(observations).notable_pairs()
-```python
-# Load data
-data = analyzer.load_data()
+leaderboard = MigrationPredictor().train(observations)
+best = leaderboard.best()
+```
-# Temporal analysis
-temporal_results = analyzer.temporal_analysis(data)
+### In the browser
-# Correlations
-correlations = analyzer.correlation_analysis(data)
+The dashboard in `docs/` runs this same package inside the page with
+[Pyodide](https://pyodide.org/): choose a seed, the number of held-out years and
+the measurement to forecast, and the study is computed on your machine. A
+separate chapter loads observed temperatures from Open-Meteo and bee occurrence
+records from GBIF.
-# Predictions
-predictions = predictor.predict_migration_patterns()
+```bash
+make site # builds the package into docs/package and serves http://127.0.0.1:8000
```
+Pushes to `main` publish the site through the `Pages` workflow.
+
## 🧪 Tests
```bash
-# Run tests
-pytest tests/
+pip install -e ".[dev]"
-# Coverage
-pytest --cov=src tests/
+pytest # tests with coverage
+ruff check src tests # linting
+ruff format src tests # formatting
-# Linting
-flake8 src/
-black src/
+node --test "tests/web/*.test.mjs" # site utilities
```
## 📋 Roadmap
@@ -330,10 +330,11 @@ black src/
- [x] **Cross-validation of results**
- [x] **Evidence-based recommendations**
+- [x] Interactive dashboard that runs the package in the browser
+- [x] Live field data from GBIF and Open-Meteo
+
### 🚧 **In Development**
- [ ] REST API for predictions
-- [ ] Interactive dashboard
-- [ ] Real-time data integration
- [ ] Cloud deployment
### 🔮 **Future**
diff --git a/data/bee_climate_data.csv b/data/bee_climate_data.csv
index 06f4fd0..c3cccd0 100644
--- a/data/bee_climate_data.csv
+++ b/data/bee_climate_data.csv
@@ -1,25 +1,25 @@
year,temperature,latitude,species_count,honey_production
-2000,22.748357076505616,-15.163314817357556,45.68723657913692,24.946260941335073
-2001,22.539563501588322,-15.097158005695736,41.12609360231801,27.129574179373186
-2002,23.041235573398172,-15.60616763844408,44.952515764876544,20.63559973516973
-2003,23.58760188472575,-15.278594942322384,43.186357178297804,25.58067984351936
-2004,22.817705921333985,-15.701930737410425,42.254851651562,24.26100538496595
-2005,22.926409782394977,-15.739682037981462,44.48422214289913,23.464532452562068
-2006,23.94178032079717,-15.963120679320992,44.97504252325277,23.833293338716295
-2007,23.644586929793846,-15.357360022908187,44.42777762953674,20.49690749766388
-2008,23.13482802442383,-16.047527428290948,40.53895625790255,22.931361733461124
-2009,23.7495408913582,-16.49122632216503,41.25114046568888,23.579147118137186
-2010,23.355247675332897,-16.057584352456,42.18426599324191,25.042928023634012
-2011,23.462787297127914,-16.80103570368696,43.12500329772298,21.831290324763444
-2012,23.925328961869972,-16.502558312802922,39.867738480831164,21.178563943486303
-2013,22.956403355931972,-17.283553211076974,40.106942916237585,21.421277478101455
-2014,23.159280214178263,-17.224542771191267,37.91776483459664,23.329624915683546
-2015,23.849291017988207,-16.897463368369696,37.39019544749083,22.232257099272136
-2016,23.732714874615397,-16.865416547740505,41.059834253484055,20.727098824784225
-2017,24.50494975325416,-17.165980819990835,41.79943657888078,22.07424897575699
-2018,24.002509701369828,-17.382520571672995,38.59511019162194,21.23257284576119
-2019,23.859065540636703,-17.568591978242004,40.39837014361014,22.32253270319064
-2020,25.40673742793904,-18.05225224928414,38.76675031096483,19.599094272227447
-2021,24.669720545408904,-17.95508369730102,36.4054126647028,19.943289388799
-2022,24.92506845017005,-18.00775684867924,38.07061729797335,19.62922907464959
-2023,24.287625906893272,-17.682863332134325,40.07607313293194,17.80472757780182
+2000,22.652358539877216,-15.128498346648932,46.35782712614379,23.620821570997585
+2001,22.088703599053666,-15.236074847755164,44.78733205202126,25.52834981173282
+2002,23.092616902251052,-15.101176809551387,44.88258662346692,24.778855995410197
+2003,23.296369314717346,-15.281671128516862,45.219098190807514,25.383554118058175
+2004,21.959265014368736,-15.397919346955987,40.69438401246258,23.489555813103895
+2005,22.392388507438408,-15.522927612141114,42.62152713250279,24.150853014875942
+2006,23.21609411462712,-15.140114415391036,41.97229816967528,24.634037764864047
+2007,23.1027482690456,-16.034967983176255,41.28746169481766,23.01424105998486
+2008,23.36116463863916,-16.197151079591027,41.66710680189446,23.946032421553507
+2009,23.05173890577843,-16.418044861952627,44.859447839860096,22.05058934926959
+2010,24.026655509170542,-16.11955399931431,39.790076899048294,22.28150618667413
+2011,24.08454814162752,-16.096090920879384,43.11046975266122,22.03608881117642
+2012,23.837363174867566,-16.59940162860081,37.46034741329013,20.597544879442527
+2013,24.476664081744886,-16.9476991170018,39.80849080959367,22.904371764656634
+2014,24.255493801560803,-17.07343132122911,40.455940912818704,21.252418228826347
+2015,23.700788551167076,-16.76134390278302,40.955053358370726,21.75787161287414
+2016,24.42350582682386,-16.863980270378097,40.85723576828136,22.242859118793646
+2017,23.868384786542023,-17.05444502385627,40.67365099213898,21.97414459013187
+2018,24.89574688978407,-17.54747899914313,38.04168029028573,22.085034185198307
+2019,24.54025443581122,-17.408612472645203,37.46660076249695,20.721836990627168
+2020,24.58148186170563,-17.573689909431696,39.75943002338387,20.017226444977247
+2021,24.442143923450203,-17.673523855763907,37.31304352414982,20.315205292336053
+2022,25.5025750171631,-17.608136584006846,34.79645344028068,17.686389653410785
+2023,24.9227352589656,-17.932921335367595,34.73342557199304,17.82933129136537
diff --git a/data/bee_geographic_data.csv b/data/bee_geographic_data.csv
index ef8fc1a..93addff 100644
--- a/data/bee_geographic_data.csv
+++ b/data/bee_geographic_data.csv
@@ -1,201 +1,201 @@
latitude,longitude,period,abundance
--10.013143387955068,-66.3229659364033,Current (2020-2023),1.071669872654698
--12.55305720468474,-58.36516258212287,Current (2020-2023),0.6387777843001224
--9.40924584759723,-57.74171613221416,Current (2020-2023),0.11605392768967243
--5.907880574367899,-61.418218153772955,Current (2020-2023),4.000852280006621
--12.936613498893344,-56.29028569332807,Current (2020-2023),3.352234878499846
--12.936547827796723,-51.767593145483694,Current (2020-2023),16.344891195636166
--5.683148737970434,-39.91051279031576,Current (2020-2023),11.389746801488666
--8.930261083388364,-53.603377497345285,Current (2020-2023),1.621303185282788
--13.877897543739808,-52.93959687421789,Current (2020-2023),2.9305661804125216
--9.829759825656142,-55.595567326129334,Current (2020-2023),5.792343365748872
--13.853670771249849,-70.35016972239234,Current (2020-2023),3.7895440294184914
--13.862919014281028,-55.21211100359373,Current (2020-2023),0.568304889162293
--11.032150913735864,-54.51815832047179,Current (2020-2023),1.197653654799729
--19.653120978631193,-35.29406310011771,Current (2020-2023),0.2765926886261773
--18.89967133005213,-56.53888771824898,Current (2020-2023),6.160447572883418
--14.249150116963891,-52.5876212613311,Current (2020-2023),1.8636954051772363
--16.051324481337694,-55.277694157641946,Current (2020-2023),0.5192074508369674
--10.743010669618904,-64.34942430095626,Current (2020-2023),2.2276590783465005
--15.632096302084845,-45.857417483879836,Current (2020-2023),1.9253408694861434
--17.649214805341167,-48.984535738505805,Current (2020-2023),0.8868409245942122
--6.137404924313784,-48.671744423655625,Current (2020-2023),0.24107824279693854
--12.903105201946143,-62.275099638357915,Current (2020-2023),2.226882567038204
--11.729887181248305,-43.77764551251121,Current (2020-2023),1.4692208483720754
--17.698992744853825,-66.21480850233824,Current (2020-2023),2.95961416192103
--14.17753089810073,-50.30514324959784,Current (2020-2023),1.4686197208470302
--11.556309641160535,-37.476354993520175,Current (2020-2023),3.823540222616968
--16.60397430968921,-62.924290601045506,Current (2020-2023),1.605508238042975
--10.497207926617312,-59.53038183682217,Current (2020-2023),1.6462291619602942
--14.402554759675219,-54.20278907929887,Current (2020-2023),4.185517286459543
--13.166774999173107,-59.027805232929595,Current (2020-2023),1.033294627976234
--14.406826448917588,-67.40530744852906,Current (2020-2023),0.2877759107805979
--4.590887261964249,-54.451496201551784,Current (2020-2023),0.05841004286418937
--12.053988898951737,-63.49842970980884,Current (2020-2023),2.8141149053251713
--16.2308437158236,-51.211260554918546,Current (2020-2023),1.9367979341213593
--8.709820351587243,-62.355393873870426,Current (2020-2023),2.435330695595753
--16.88337459988409,-42.60052475985968,Current (2020-2023),0.47896298698688694
--11.164545619980979,-61.2660263386899,Current (2020-2023),0.2932251024565573
--19.838680495519103,-57.5764921296454,Current (2020-2023),0.029302952531337564
--17.312744195593723,-48.49186226104264,Current (2020-2023),0.8633745230301385
--11.212555056523506,-64.84691453147164,Current (2020-2023),1.7827947513298892
--9.046133680018357,-53.18032052316696,Current (2020-2023),0.9959637351223014
--11.314526875240118,-44.54285796574057,Current (2020-2023),1.1506391256568624
--12.462593129552962,-67.85986587648982,Current (2020-2023),4.690123045962883
--13.204414782357155,-53.52292913174156,Current (2020-2023),0.8562052288502234
--17.91408796146971,-52.92093764601261,Current (2020-2023),1.4430500561446422
--14.879376833578835,-48.74541702578152,Current (2020-2023),3.0617434754394033
--13.84255508383915,-64.89560568702466,Current (2020-2023),1.0101602625983594
--7.771511095124337,-65.56365290467421,Current (2020-2023),1.9461809507061707
--10.625526841726154,-50.82446747506482,Current (2020-2023),3.9662812873686133
--19.052160621450938,-52.624122614134514,Current (2020-2023),5.972380830749335
--10.70366412242082,-52.996057197232986,Current (2020-2023),0.31816375862967194
--13.540329121665266,-52.22841432402419,Current (2020-2023),5.223325526084158
--14.707688001223834,-60.440197772627926,Current (2020-2023),1.3550055619910613
--9.553294844636529,-53.14197042271197,Current (2020-2023),0.5974709087027915
--7.876001910016196,-52.65542021361055,Current (2020-2023),1.229173934741395
--8.274879523535205,-60.71481134421094,Current (2020-2023),7.827306195161183
--15.356870092890555,-40.073803910841946,Current (2020-2023),1.3569825853347195
--13.236849503404859,-51.2093366327057,Current (2020-2023),0.7972320642009996
--10.674946274385745,-64.53042797762119,Current (2020-2023),2.006972547915319
--8.097819491510563,-49.74757113092936,Current (2020-2023),0.5492569354312059
--13.91669695138116,-62.79745336181857,Current (2020-2023),0.15779060995247007
--12.742635906655268,-48.703323170060386,Current (2020-2023),0.2759504394248317
--16.425339896024113,-45.73123536794077,Current (2020-2023),0.27403684813401424
--16.784826496322683,-61.56545854681369,Current (2020-2023),0.32951980311334594
--8.749896710423208,-47.29299096604542,Current (2020-2023),0.2989201321350331
--6.575039885716708,-51.69775258450802,Current (2020-2023),2.048168102151613
--12.288040486321336,-48.42351872004408,Current (2020-2023),0.40149271406896325
--7.985868408431903,-39.82565613876842,Current (2020-2023),0.8482786315896105
--10.553455899809464,-56.963104928022965,Current (2020-2023),4.5419482502567154
--14.580479018420498,-61.029889314859915,Current (2020-2023),1.2847622832931873
--10.554417577966344,-62.11611543700418,Current (2020-2023),2.202578167304761
--5.847853734136123,-61.52648227972351,Current (2020-2023),0.378257034226433
--12.143304156439806,-55.616813675312834,Current (2020-2023),0.4271019619901901
--5.741425376743976,-52.27078420146685,Current (2020-2023),0.08345442543513061
--22.47898041635898,-52.786473605359845,Current (2020-2023),0.37009468813116175
--8.712389982499104,-48.38253400771181,Current (2020-2023),0.653096237262291
--11.651811727047315,-54.89598486497675,Current (2020-2023),0.3896236349662159
--13.196029401863472,-43.37172738274147,Current (2020-2023),0.18577181572073515
--11.63295689385799,-57.11725466590365,Current (2020-2023),0.25711242698670506
--19.95027565840357,-33.23864666728305,Current (2020-2023),1.2352586867307582
--12.878687551350048,-49.99466121787995,Current (2020-2023),0.46218441269019317
--10.571549713953015,-61.85726045133026,Current (2020-2023),0.905962231021837
--6.088423821033936,-63.5671399844889,Current (2020-2023),1.4000103694286608
--14.07308087309459,-51.14022067805452,Current (2020-2023),2.3449148680250844
--15.233974411572751,-56.78770228260681,Current (2020-2023),0.08021146020003085
--14.007028174338146,-49.28799604726326,Current (2020-2023),3.212988487652239
--8.338391529191703,-51.21409900341164,Current (2020-2023),1.9771873807345928
--10.684995561361262,-55.582631301254985,Current (2020-2023),0.17059086075266852
--14.119040815068155,-61.77434974454724,Current (2020-2023),4.136269396976181
--9.946930267546575,-67.11877779748691,Current (2020-2023),5.073387092757856
--11.611689802607838,-58.57211961653617,Current (2020-2023),0.1260456551203268
--8.125420037868443,-48.14880964541222,Current (2020-2023),0.6483536862573016
--14.80821237550941,-53.28725004695837,Current (2020-2023),3.281870366453285
--13.310648586391073,-64.96591022969591,Current (2020-2023),2.7587144804286168
--13.56843261252863,-53.614552593190545,Current (2020-2023),0.40795926412360095
--17.854059792528474,-51.91746096216931,Current (2020-2023),0.4697980623630665
--10.815518891741696,-62.07085948960906,Current (2020-2023),0.9255702200487395
--10.955778911280444,-53.77019915243578,Current (2020-2023),1.3253251293372288
--11.979546173430156,-54.534330252432,Current (2020-2023),1.926003668117104
--12.938348533500589,-64.14376238264498,Current (2020-2023),0.9206251260007023
--16.56885055860687,-64.63196008737658,Projected (2040-2050),0.9313366638313765
--15.756861894527063,-62.481448321575755,Projected (2040-2050),2.0643434105847636
--13.667795027298892,-52.021651159013906,Projected (2040-2050),0.05605942848221769
--13.784791791860389,-53.11703787653228,Projected (2040-2050),0.4364049328235958
--23.510677471828366,-58.16721275171319,Projected (2040-2050),1.8742378124340386
--21.75130015966049,-57.06138093352975,Projected (2040-2050),3.38365013149592
--15.939858931165361,-47.778680833692604,Projected (2040-2050),1.0751686683126014
--15.944856196351164,-62.73257111068664,Projected (2040-2050),1.1392942535296033
--15.93980925477581,-53.6232209506397,Projected (2040-2050),0.17004200781099507
--2.589074037381117,-59.61754121947115,Projected (2040-2050),0.8897148691253263
--15.716437957227331,-59.741449625817765,Projected (2040-2050),1.1409101985829349
--13.457737439277604,-49.20978518410248,Projected (2040-2050),0.41653920489097995
--14.18399294602719,-51.39666920809576,Projected (2040-2050),0.47051191781781576
--15.394434994776809,-51.49192291199489,Projected (2040-2050),0.710497477203708
--19.261076978561384,-47.55616954276537,Projected (2040-2050),0.030413041125471774
--14.964123118026931,-57.831969266937925,Projected (2040-2050),0.5830871418842036
--21.091300858150287,-52.54437622964029,Projected (2040-2050),0.3563354034354448
--18.947274426960035,-60.48213405274765,Projected (2040-2050),0.595123845347974
--19.941454191316414,-55.406669180092464,Projected (2040-2050),0.1913446549271719
--17.67250344245471,-59.04114443494148,Projected (2040-2050),3.3181198499145372
--8.741365733305965,-57.22403228005825,Projected (2040-2050),1.3505982208193397
--25.469060770366994,-53.23874379650469,Projected (2040-2050),1.7049494368395475
--15.254959238501947,-64.54576546586777,Projected (2040-2050),2.335063548566687
--24.450863484758607,-41.260901794516315,Projected (2040-2050),1.0350546327733277
--19.887727463157734,-66.04813905199762,Projected (2040-2050),0.13639814177618376
--13.644197612130535,-67.71350890230185,Projected (2040-2050),1.1553875449422337
--17.742879923618148,-48.73511301199946,Projected (2040-2050),1.3258845868070603
--22.310979111717224,-51.666698448296515,Projected (2040-2050),2.052324973212614
--20.861214837039874,-53.00704146358276,Projected (2040-2050),0.8475519740744731
--15.281609004261297,-52.97323592588576,Projected (2040-2050),0.20472700009396833
--20.921466526868546,-58.09797418277532,Projected (2040-2050),0.5006432703925559
--17.1341656416721,-65.17803497188665,Projected (2040-2050),0.6766722382119352
--17.817712640384745,-57.39356353445019,Projected (2040-2050),1.5573369174327925
--20.60640139042327,-63.41729369209689,Projected (2040-2050),1.2686725818021294
--9.424223642698697,-50.19904213265799,Projected (2040-2050),0.6603101394777092
--15.464323910727956,-59.17645905201711,Projected (2040-2050),6.459293658254719
--26.100570346630427,-64.60397757434009,Projected (2040-2050),1.3962495127008139
--17.25418274092229,-60.57108673322395,Projected (2040-2050),0.40619179313519993
--20.647145859073554,-54.696548365795,Projected (2040-2050),0.16101450693733027
--14.590266660815104,-62.5097964224318,Projected (2040-2050),0.24883243888473883
--21.1700829537308,-64.57776316453145,Projected (2040-2050),0.4234602740927114
--18.458945765867597,-56.0505023080647,Projected (2040-2050),0.26274731243144106
--15.980050884078171,-56.04026743113022,Projected (2040-2050),0.3097376181818706
--14.536979223319515,-62.05554540296904,Projected (2040-2050),0.5034087723231698
--22.801185628223106,-61.76830644494658,Projected (2040-2050),0.2856036504943305
--19.338004943363792,-56.14360050113891,Projected (2040-2050),3.406127174033873
--19.899781244643826,-69.5846747319786,Projected (2040-2050),0.12545356888800024
--20.613316930294847,-69.25971019501245,Projected (2040-2050),1.115118525169054
--10.938183038875612,-63.747553770019486,Projected (2040-2050),0.7924583347339536
--16.38007315615618,-59.707577213694776,Projected (2040-2050),6.057963249023881
--23.04353581734018,-55.512739475215966,Projected (2040-2050),0.17824101854610735
--14.328552211780895,-46.19715026440358,Projected (2040-2050),0.7608869906136456
--9.511375211949467,-51.13872301438384,Projected (2040-2050),5.233589496190072
--13.870138957795412,-59.27950823970742,Projected (2040-2050),3.009366092952232
--24.077479863816052,-58.15212966322151,Projected (2040-2050),2.547994551054269
--19.936936291465006,-66.02023491710247,Projected (2040-2050),0.44741262530025694
--12.93235540325351,-58.14810508793912,Projected (2040-2050),0.2810992991797313
--20.830677862475124,-60.309269111361104,Projected (2040-2050),1.6568393980226754
--16.224722287415087,-55.41825151729528,Projected (2040-2050),3.975577642439933
--14.901463786282653,-64.61784754841858,Projected (2040-2050),1.2204756328571134
--21.70772188631233,-53.845227886070624,Projected (2040-2050),1.2715913421878797
--18.2381014242472,-45.73808869597938,Projected (2040-2050),0.49271254621597627
--30.96506936027629,-58.87008118765486,Projected (2040-2050),2.2012106376181326
--22.09755056533716,-54.78630622320847,Projected (2040-2050),0.31061697321910625
--19.01027260557264,-52.4788480663111,Projected (2040-2050),0.586631719678849
--22.991132727859398,-61.20976377508669,Projected (2040-2050),0.8312168251047458
--11.470354784273459,-56.20726014551666,Projected (2040-2050),1.0627274439670478
--23.72056551184253,-57.89926079374564,Projected (2040-2050),0.41641887535440875
--19.760177946787934,-57.21859121160934,Projected (2040-2050),0.18297490821620316
--17.477037690855635,-64.18407827084373,Projected (2040-2050),1.4147994861440347
--12.234906843735537,-57.80391860592846,Projected (2040-2050),0.5108450526548127
--23.743448604717756,-54.0160136700364,Projected (2040-2050),1.3056796024386654
--13.347344991380162,-46.390851137639665,Projected (2040-2050),0.2514971291560197
--17.95906775592165,-50.32583339131835,Projected (2040-2050),0.9841820670427913
--21.926034604191805,-40.774540339907546,Projected (2040-2050),1.1408208721936166
--16.15158610294692,-64.1387805031044,Projected (2040-2050),0.07982197718568199
--17.20376121770612,-51.021434906234575,Projected (2040-2050),0.6155754020706349
--20.40086750863518,-56.53326395409319,Projected (2040-2050),0.21652399024789742
--17.720791660039925,-40.48157653425862,Projected (2040-2050),0.09820838815878732
--19.54125438744704,-64.46638628284121,Projected (2040-2050),6.901801967092986
--17.54593061899501,-64.7177747374462,Projected (2040-2050),0.5836950337791303
--15.351477301915814,-62.795141163552174,Projected (2040-2050),2.490105920654848
--11.655932735418592,-74.99116579447846,Projected (2040-2050),0.4408332514401021
--22.951261995307398,-62.20604017344609,Projected (2040-2050),1.7162120265305272
--9.467866501374933,-64.07306129242959,Projected (2040-2050),2.142099334242313
--25.80835119809001,-56.79684970819034,Projected (2040-2050),1.3581698912537097
--18.607140380142333,-55.265952193782724,Projected (2040-2050),0.9567849660679076
--15.646731174061696,-42.99063328627291,Projected (2040-2050),0.7961367202580667
--16.87603252905987,-50.3966092945116,Projected (2040-2050),0.6435649525559622
--20.490798079282374,-62.615229245299226,Projected (2040-2050),3.9788340723311713
--18.832489001429103,-65.18731737078686,Projected (2040-2050),2.663410606712697
--19.972003738635333,-54.06464662794795,Projected (2040-2050),5.029764584123537
--20.357459027776848,-68.56186565616514,Projected (2040-2050),0.19909281275415833
--14.6015916119159,-43.34832987316517,Projected (2040-2050),1.9688270451907612
--16.57193805613981,-48.564479034229706,Projected (2040-2050),4.179190897891861
--20.771638381042617,-61.753405216837635,Projected (2040-2050),0.29993371455119655
--14.401600498266998,-71.70507623272702,Projected (2040-2050),0.1032154762087453
--16.770801916493564,-47.169021006676694,Projected (2040-2050),2.027089729782933
--14.74855152464416,-58.91631876202094,Projected (2040-2050),1.2816407162980046
--15.481484632305552,-48.097469504212306,Projected (2040-2050),2.766115711982217
+-10.781131680982275,-58.02530043231512,Current (2020-2023),0.016258845666068018
+-16.159936424961984,-44.60617361771148,Current (2020-2023),0.8760470311359875
+-8.998195216774171,-57.85011176849141,Current (2020-2023),0.30125390159704935
+-8.237741134435144,-49.099875452263305,Current (2020-2023),4.1349241639966765
+-19.804140754615347,-62.468941440079014,Current (2020-2023),0.21317639239357392
+-17.208718027449272,-56.64350046294104,Current (2020-2023),1.368485180420787
+-11.488638387330859,-62.60017643928465,Current (2020-2023),2.0350549347436537
+-13.264970369374328,-57.7122646072045,Current (2020-2023),5.740010787713649
+-12.067204630017155,-48.27753490034084,Current (2020-2023),0.5809563127769198
+-15.41217571029432,-68.8185633855388,Current (2020-2023),3.4715597994257856
+-8.482408100548685,-51.52461085163314,Current (2020-2023),1.5273387440243944
+-8.888832258284207,-53.098115181341775,Current (2020-2023),3.4600400841055454
+-11.735877209755136,-59.753199645574355,Current (2020-2023),0.7162601359535599
+-7.4910351721278685,-66.56846283510764,Current (2020-2023),0.66553797868849
+-10.129962630991818,-54.422963938289044,Current (2020-2023),0.50554605101063
+-15.437169851532953,-59.23594167251042,Current (2020-2023),2.087793643821079
+-10.524996863670005,-53.13859030916237,Current (2020-2023),1.2992532201905334
+-15.835530403315996,-54.825182835812456,Current (2020-2023),1.73215862493018
+-8.48619879477091,-42.18576886943268,Current (2020-2023),2.565656749770563
+-12.199703643945012,-56.914845019784195,Current (2020-2023),2.3081548739880975
+-12.739449454181042,-63.18797994097492,Current (2020-2023),2.0905965732892224
+-14.723718177615766,-53.56579492034947,Current (2020-2023),2.7974952794850187
+-7.109834645303879,-53.240026528225876,Current (2020-2023),0.06887364689094967
+-12.61811792827521,-44.126499398076504,Current (2020-2023),3.0098797077726025
+-13.713311288652429,-48.31911003268337,Current (2020-2023),2.6008225532467137
+-13.408534201952918,-52.145031526803926,Current (2020-2023),0.8773262862489419
+-9.870763257786605,-43.293576870243506,Current (2020-2023),0.4794547134592707
+-10.538223742543687,-64.51010443458281,Current (2020-2023),1.4212288313194457
+-10.349069553616047,-60.11801226199798,Current (2020-2023),4.051391492277269
+-10.276715987968469,-62.4126075312442,Current (2020-2023),0.6518561271245585
+-3.433409596518155,-58.118478425246145,Current (2020-2023),2.8665784761039945
+-13.625660065538462,-66.01348918045048,Current (2020-2023),5.938115777309932
+-14.04897091628615,-49.91879242548477,Current (2020-2023),0.7976493413890025
+-15.25509091299151,-56.77778157679019,Current (2020-2023),8.605544321459687
+-9.536082309698017,-66.76645035602127,Current (2020-2023),0.12079196545117699
+-7.4841108291164335,-63.124632649660334,Current (2020-2023),0.07436995125487848
+-12.4557898306195,-52.491889220398434,Current (2020-2023),3.7146174482119547
+-15.360625907850112,-48.29498745684495,Current (2020-2023),1.290776310699815
+-15.297924862764958,-39.02615286646571,Current (2020-2023),1.2120922026906917
+-9.397628848701196,-31.689100271941363,Current (2020-2023),3.918354582968189
+-9.02698331518623,-51.68472453379203,Current (2020-2023),4.949999413316079
+-9.82738292677922,-62.91630496025491,Current (2020-2023),1.6485568050579071
+-14.662038829154778,-72.05637024585047,Current (2020-2023),4.213110778032889
+-11.071354707733121,-52.858308301249316,Current (2020-2023),2.02093969448452
+-11.533256763437087,-61.50352876248261,Current (2020-2023),1.0029259259940264
+-11.125245613083948,-58.32285808143748,Current (2020-2023),0.48658503328379055
+-8.514284888207241,-59.89677439247846,Current (2020-2023),2.753530491986417
+-11.105617804901271,-56.126327091331085,Current (2020-2023),4.281767287393165
+-9.28434574771242,-46.47215815369885,Current (2020-2023),5.339773929724723
+-11.729683722044435,-53.74361146043724,Current (2020-2023),0.6040212947625359
+-10.843522405240064,-56.269078696309506,Current (2020-2023),2.0515993673276363
+-9.474847096645838,-63.28523002260649,Current (2020-2023),1.0266068614593968
+-17.828623279422665,-68.39746355763485,Current (2020-2023),3.423856839562516
+-13.278684865429206,-58.89046327258664,Current (2020-2023),0.4414289278629045
+-13.881490617171181,-55.430260406546566,Current (2020-2023),0.11789296972073693
+-14.555511392973369,-40.85656069136094,Current (2020-2023),0.7218281945805592
+-13.100569004906735,-53.95780382821691,Current (2020-2023),0.14132072856515226
+-6.0202347550624165,-47.13808391181554,Current (2020-2023),0.942207398245028
+-15.463324462772974,-58.99436478831322,Current (2020-2023),0.9757600163559004
+-8.126886581634077,-64.4795501313362,Current (2020-2023),0.22677838435763056
+-18.731479086463217,-62.72093409785897,Current (2020-2023),6.174460553672845
+-13.339540119943099,-60.80180851628602,Current (2020-2023),0.6043606662065348
+-11.348987739579977,-37.97224214051869,Current (2020-2023),2.3935356533337107
+-9.655110674562888,-61.57109343379509,Current (2020-2023),0.40144114545920534
+-9.15509368082858,-48.29208637010924,Current (2020-2023),1.0175339123669354
+-8.8266110592003,-62.22341742469621,Current (2020-2023),0.18048742736891324
+-13.39490028899375,-47.54741589700605,Current (2020-2023),0.23568394192542336
+-13.849407170658269,-51.9203922711531,Current (2020-2023),0.022302471622178473
+-8.568096474971384,-56.25310318126472,Current (2020-2023),3.230734123426393
+-12.76521729952646,-55.32610020908347,Current (2020-2023),0.7105094718765531
+-17.10274529335169,-60.23830156343512,Current (2020-2023),1.385265036535857
+-16.533148856013923,-51.43142238814335,Current (2020-2023),1.737892979457169
+-15.677809144006446,-58.63986784272624,Current (2020-2023),0.3596260598375854
+-10.011357023784944,-64.80484611013799,Current (2020-2023),5.8662939382890995
+-11.43029705571774,-65.22350059455695,Current (2020-2023),1.093288720864789
+-9.238058583728927,-53.61929665822304,Current (2020-2023),0.1507504587823232
+-13.709010585346137,-42.36726994871652,Current (2020-2023),0.21608786259974852
+-11.365841235693143,-53.720067091412496,Current (2020-2023),0.9926666099292496
+-9.497638424130653,-55.94910660887906,Current (2020-2023),0.25398228377267584
+-13.237386158880954,-52.713390883179656,Current (2020-2023),0.6526530881537774
+-10.172899049770354,-44.5519860663454,Current (2020-2023),1.4866695443399625
+-14.647703764266605,-53.24493998908915,Current (2020-2023),0.08299421181661466
+-13.452215386260287,-58.2874178466699,Current (2020-2023),0.7177895090440346
+-13.526951575993316,-46.149690319520886,Current (2020-2023),3.1397763164900843
+-16.783358582356158,-51.56994849230709,Current (2020-2023),0.14904341956143546
+-10.052110076857673,-42.713952064032064,Current (2020-2023),1.9407159481638412
+-13.877609360810895,-53.53412450222475,Current (2020-2023),0.10093969190679737
+-11.95002352508925,-64.79575225376401,Current (2020-2023),5.356217557034076
+-10.077013364376365,-65.94527359396533,Current (2020-2023),0.5083917663642766
+-10.213875295880223,-41.79257654215,Current (2020-2023),0.4901642119826188
+-9.338459564108856,-41.21067423373363,Current (2020-2023),2.180214052256793
+-12.393941938037694,-56.436153706260804,Current (2020-2023),4.518733803065756
+-13.693193248176614,-58.0654985690879,Current (2020-2023),6.375473858729621
+-12.318872843625597,-43.30844566206238,Current (2020-2023),2.7634523845635965
+-18.74933773583212,-63.8563654563479,Current (2020-2023),3.455223259685605
+-17.788449889692348,-62.157816151646614,Current (2020-2023),2.1936925153758664
+-17.290798449417608,-49.853385642487645,Current (2020-2023),0.38507316035781414
+-15.988987310405927,-58.156840982876716,Current (2020-2023),2.3313052200512545
+-10.400903093106253,-55.04097493376057,Current (2020-2023),0.3337605880255927
+-15.621916221440243,-56.3075431881961,Current (2020-2023),1.9783254039368734
+-28.26663376372519,-54.32491382626711,Projected (2040-2050),1.369571956210157
+-18.947401058038736,-52.38437099022984,Projected (2040-2050),0.5601376483649699
+-17.293950314510212,-56.894068543530544,Projected (2040-2050),0.9178616096187973
+-16.8160240412516,-51.91893531699317,Projected (2040-2050),0.9385892635177471
+-19.48765832528516,-56.166309007079455,Projected (2040-2050),0.03623955076377002
+-25.02688712991433,-53.75948235518827,Projected (2040-2050),3.309819457257755
+-16.688018065143563,-63.637386102959056,Projected (2040-2050),6.083944544826735
+-11.09059914334326,-59.43689131065167,Projected (2040-2050),1.0268455605520144
+-24.13544561966455,-56.425791226744344,Projected (2040-2050),0.6195883398672161
+-14.544687945207247,-51.43577219666065,Projected (2040-2050),3.4061080872234557
+-19.314100892491574,-61.149929377796575,Projected (2040-2050),0.8042375857844677
+-18.24529738331539,-53.830661954143096,Projected (2040-2050),1.4601740890391268
+-22.211594042815797,-60.12671033532303,Projected (2040-2050),3.103291940499381
+-19.337824689423506,-58.94033733862504,Projected (2040-2050),0.6461431440266687
+-12.799821621204167,-51.36384766380742,Projected (2040-2050),2.8886515733330715
+-15.669379032049171,-73.94448296843325,Projected (2040-2050),1.7149485184361297
+-11.070753578360224,-68.3717786245981,Projected (2040-2050),1.5396170950627193
+-13.290352444089226,-69.85748317954257,Projected (2040-2050),2.648375376761024
+-16.243653284281695,-76.66892895878644,Projected (2040-2050),0.021926819335510862
+-11.02426189532883,-63.426115521241414,Projected (2040-2050),0.5599826603312918
+-16.244027365046804,-52.00452880217808,Projected (2040-2050),0.3383227479342478
+-14.688047292811763,-60.2790725310606,Projected (2040-2050),0.4929589956384966
+-19.186283814138832,-56.417679344465185,Projected (2040-2050),0.6333212541075095
+-17.73381673656318,-49.286260026313656,Projected (2040-2050),0.38616303115110495
+-20.789695293201106,-47.378510941858465,Projected (2040-2050),0.8943783348725045
+-14.041664261620587,-58.55310347780912,Projected (2040-2050),0.5294256056168666
+-22.713214492459464,-47.17131288344482,Projected (2040-2050),0.841413349220927
+-14.870598630329354,-57.262986732527125,Projected (2040-2050),0.9762475837927129
+-18.76260423002064,-64.6991857906197,Projected (2040-2050),0.4065745673543105
+-13.315011626130723,-62.75520281758988,Projected (2040-2050),1.4553120054992439
+-14.99652404507496,-69.84429210052014,Projected (2040-2050),1.763725633636652
+-10.717415369412794,-65.10507082986922,Projected (2040-2050),2.3365775609746597
+-15.07690123544542,-60.864133504595436,Projected (2040-2050),0.3445064262414205
+-24.28816102261296,-51.571319844971185,Projected (2040-2050),1.4880015686794414
+-18.267812691592802,-44.23384135067213,Projected (2040-2050),2.0579543994372402
+-22.688028770379262,-69.05745212301632,Projected (2040-2050),0.07149175691654544
+-20.073119369465097,-54.857380253920645,Projected (2040-2050),0.855452351738509
+-11.955086266893113,-66.32435151326027,Projected (2040-2050),1.4260004056247904
+-15.449864756409216,-54.202423292294384,Projected (2040-2050),0.44643973153815664
+-20.795721714314986,-59.04869320550181,Projected (2040-2050),2.757704641530205
+-22.054869465761595,-72.64724660678024,Projected (2040-2050),1.9428021181824733
+-17.868871628494958,-50.57362406745212,Projected (2040-2050),0.28462531895341886
+-22.86624059752451,-62.840005702469846,Projected (2040-2050),0.8951851027121704
+-20.684561109586326,-62.27120190698844,Projected (2040-2050),5.029397267779108
+-16.751962113795024,-66.55801929031752,Projected (2040-2050),1.196834119580291
+-13.378751855573215,-63.23426621350045,Projected (2040-2050),2.1328820619382194
+-15.56495411110224,-54.57687647444066,Projected (2040-2050),3.7107824802503826
+-27.165158034392398,-59.513954727491246,Projected (2040-2050),0.8893518406138772
+-16.78253308387896,-55.37070398174015,Projected (2040-2050),0.1455922496309293
+-17.711865707069688,-55.10462516856925,Projected (2040-2050),0.37212780323015965
+-16.34443885850923,-47.43470675577466,Projected (2040-2050),0.9950220122723459
+-11.535161273395172,-60.74228920691503,Projected (2040-2050),0.8802023695776031
+-26.25295310721897,-69.81486253476585,Projected (2040-2050),1.216813043439858
+-20.364413700764423,-49.462220667424134,Projected (2040-2050),1.978025602268723
+-15.636374708826581,-60.65190537677804,Projected (2040-2050),2.28048658372997
+-24.32637759781577,-49.083260443380986,Projected (2040-2050),1.5993402019985634
+-12.096203807785965,-54.93298309454023,Projected (2040-2050),4.2905333867528705
+-16.526573549197366,-59.04910024162676,Projected (2040-2050),5.4226677996717365
+-14.613664056626384,-55.209792847629636,Projected (2040-2050),1.9576260744294558
+-20.283774646087178,-42.391899518989604,Projected (2040-2050),2.919980371781085
+-14.744945240135158,-41.38415576756998,Projected (2040-2050),0.7727014963619003
+-13.726113776043846,-57.44495091893777,Projected (2040-2050),0.21635216471469831
+-17.068487919451325,-56.718475253463396,Projected (2040-2050),1.2625106454521182
+-17.062396429205215,-49.39007874026892,Projected (2040-2050),3.88546439531219
+-16.918627042873098,-64.76528826213878,Projected (2040-2050),0.24002381049298405
+-21.453381058957525,-55.335437019053956,Projected (2040-2050),0.6571038206259527
+-18.590114720961413,-58.20690278363085,Projected (2040-2050),1.4972578854097853
+-18.610090129809436,-55.48873430833957,Projected (2040-2050),0.8614613913584263
+-16.46642454258602,-64.66695044759524,Projected (2040-2050),0.08037466552932865
+-14.000703012358821,-70.71653994647176,Projected (2040-2050),6.232828860945565
+-22.234144327913636,-74.58386748793033,Projected (2040-2050),1.1656244505949511
+-18.50003612127831,-66.93907290391687,Projected (2040-2050),0.43819834948091885
+-12.074177809364107,-61.669402279514664,Projected (2040-2050),0.7466148134909655
+-20.974352915250257,-60.345532693190094,Projected (2040-2050),0.044196233008469185
+-21.289000069161283,-42.50215070056387,Projected (2040-2050),1.207622842216226
+-17.190775232675225,-49.15205304074161,Projected (2040-2050),2.4687854800276114
+-14.62245923805872,-65.69672893073302,Projected (2040-2050),1.2105961923354578
+-17.954295777757363,-55.21833238039235,Projected (2040-2050),2.22743225777887
+-12.684157638252042,-61.2566258908026,Projected (2040-2050),1.013251924286748
+-14.572824069733892,-60.27491070432076,Projected (2040-2050),2.185226854089137
+-14.632719732439753,-56.51739480467695,Projected (2040-2050),1.0605112259472325
+-15.783533994703626,-53.04663106419685,Projected (2040-2050),1.5336798884298326
+-8.68938759861566,-60.714067871072125,Projected (2040-2050),2.883764633271947
+-18.82064675546418,-49.48918773812513,Projected (2040-2050),6.587984871405886
+-26.014089168266054,-67.13550580913923,Projected (2040-2050),0.8071010454195583
+-11.582982553931437,-57.94928750110185,Projected (2040-2050),5.1677913826761275
+-19.830797723827303,-37.21861018723334,Projected (2040-2050),0.16806965778780675
+-17.568478222627867,-56.2153620589978,Projected (2040-2050),1.3627789401707764
+-12.761797260557891,-46.53428394715062,Projected (2040-2050),1.0537882374302106
+-24.409038164235703,-57.26783857461434,Projected (2040-2050),1.2943296712603434
+-23.006588856424557,-53.353783237361625,Projected (2040-2050),2.770508286073297
+-24.405111748475413,-58.45426555514153,Projected (2040-2050),0.7407622172722015
+-21.176545160101696,-59.36326064931364,Projected (2040-2050),6.302150121352524
+-16.241453575932027,-64.23585917380336,Projected (2040-2050),1.191059642881807
+-15.903248628748916,-54.55758912833957,Projected (2040-2050),2.1929011816153903
+-16.894903282685785,-64.81229751348614,Projected (2040-2050),1.334135883428593
+-23.651063535569467,-52.675318109492565,Projected (2040-2050),1.105634988106091
+-27.240413746530578,-49.317703964432724,Projected (2040-2050),0.862968246308531
+-17.78258565843847,-55.06774874210216,Projected (2040-2050),0.0365628870292898
+-19.887104134553343,-60.28998986844554,Projected (2040-2050),5.8933746333551955
diff --git a/data/processed/bee_climate_processed.csv b/data/processed/bee_climate_processed.csv
index af4e2f9..62c9a04 100644
--- a/data/processed/bee_climate_processed.csv
+++ b/data/processed/bee_climate_processed.csv
@@ -1,121 +1,121 @@
-year,region,latitude,longitude,temperature,precipitation,bee_abundance,species_diversity,honey_production,date,temperature_anomaly,precipitation_anomaly,climate_stress_index,bee_health_index
-2000,Norte,-2.208968052956953,-59.480234411503154,26.79802849180674,2145.561727547482,84.92690609790323,44.928347921780095,28.59224055412915,2000-01-01,-1.0133885396450815,46.857371943672206,23.935380241658642,54.51682620686233
-2001,Norte,-3.909387454794739,-58.43635425644538,26.53878054973207,2204.5705198405517,75.31523400579503,45.47265077092174,28.26124097914239,2001-01-01,-1.2726364817197506,105.86616423674195,53.569400359230855,52.93890426346218
-2002,Norte,-1.5972056890639008,-62.4565554805595,27.132091383729982,2071.8571639969,84.88128941219136,39.064857617907464,27.48848778285014,2002-01-01,-0.6793256477218392,-26.847191606909746,13.763258627315793,53.91851938510382
-2003,Norte,-4.4018510627922804,-62.4565554805595,27.779035305149165,2217.8207623811772,80.46589294574451,45.47265077092174,26.748217037597534,2003-01-01,-0.03238172630265623,119.30022883945276,59.66630528287771,53.89092031394133
-2004,Norte,-2.4131429061997296,-58.9561168687662,26.846464496905128,2113.8491744863804,78.13712912890499,43.782789788650256,24.125780255029603,2004-01-01,-0.9649525345466934,15.14481888257069,8.054885708558691,51.62742266466595
-2005,Norte,-1.4780222128579334,-59.40603065353363,26.968213478004404,2138.2219293684984,83.71055535724783,42.662854864285656,25.854361528382324,2005-01-01,-0.8432035534474167,39.51757376468868,20.180388659068047,54.039387060699525
-2006,Norte,-3.9905363251306882,-59.49901429930825,28.177962471913133,2100.698903994452,84.92690609790323,43.09656503133187,25.749353575642115,2006-01-01,0.3665454404613122,1.9945483906421941,1.5727635858421642,54.51682620686233
-2007,Norte,-3.566297729602772,-59.30710358100605,27.812634750535224,2217.8207623811772,83.56944407384186,38.59007956210256,24.569358505035023,2007-01-01,0.0012177190834030682,233.38919629027632,120.43625332656191,52.375609049678026
-2008,Norte,-2.900348634912359,-61.36004944315698,27.19222841191729,2146.476364482728,73.84739064475637,41.7780475286728,25.590472054928853,2008-01-01,-0.6191886195345297,47.772008878918314,24.19559874922642,49.74951213298305
-2009,Norte,-3.5034756541161993,-59.535492605677995,27.921188200064623,2035.533254930497,75.62785116422219,42.583790360414795,26.460275626672555,2009-01-01,0.10977116861280223,-63.17110067331282,31.640435920962812,50.96436026181508
-2010,Norte,-4.550663431066132,-59.413855053402635,27.439340688660348,2217.037099905971,77.96066498310478,44.47752721991781,29.163676150247145,2010-01-01,-0.3720763427914733,118.33274430216125,59.35241032247636,53.27662700429139
-2011,Norte,-2.9314370251939725,-61.428702836052736,27.55969258264045,2006.1765045681152,80.31250824430745,40.13737260693097,25.479590408272372,2011-01-01,-0.2517244488113697,-92.52785103569454,46.389787742252956,51.81009220228398
-2012,Norte,-4.062303713726105,-56.268450977710486,28.10604692815701,2142.625489935258,72.16934620207789,39.20909975073537,25.384665998836834,2012-01-01,0.2946298967051888,43.92113433144823,22.10788211407671,48.24586820570282
-2013,Norte,-2.5264075693648183,-59.05233415817642,26.934640548857494,1919.2503789163704,72.76735729059396,39.474746782396146,24.45980382064158,2013-01-01,-0.8767764825943267,-179.4539766874393,90.16537658501682,48.2873080971489
-2014,Norte,-3.9194242342338033,-62.3826069944053,27.169397126579135,1975.87704728407,67.29441208649159,41.96123901801285,20.51028365635844,2014-01-01,-0.6420199048726865,-122.82730831973981,61.73466411230625,45.65922163690802
-2015,Norte,-1.4780222128579334,-58.68689278273234,27.988714438977155,2121.860036630391,65.97548861872708,40.44011091497154,24.033928770840696,2015-01-01,0.17729740752533374,23.155681026581078,11.666489217053206,45.73240735323451
-2016,Norte,-3.783253292336237,-61.949363340454646,27.840127414755866,2169.4988319125846,75.14958563371012,38.375262201161576,23.946547376403792,2016-01-01,0.02871038330404474,70.79447630877485,35.411593346039446,48.75637712675366
-2017,Norte,-3.3220615162056757,-58.42583079251509,28.758113616948467,2106.2672629016056,76.99859144720195,40.113491387965844,28.49170161627492,2017-01-01,0.946696585496646,7.562907297795846,4.254801941646246,51.38099448015301
-2018,Norte,-2.18648278263033,-57.68280884198519,28.14648990251336,2071.0438674133497,68.98777547905485,38.93328553347869,22.91962589652471,2018-01-01,0.3350728710615378,-27.66048819046,13.99778053076077,46.15098362062296
-2019,Norte,-4.230864316433955,-61.64136463670342,27.965661257459693,2045.9765869628102,73.49592535902534,40.32859432889187,23.646572945536793,2019-01-01,0.15424422600787224,-52.72776864099956,26.441006433503716,48.59092032593874
-2020,Norte,-2.7725400653958707,-58.07324774151136,29.19883947437903,1921.7130183545617,69.41687577741207,36.639372073114856,22.713185156241686,2020-01-01,1.8507194927950394,-176.9913372492481,89.49704604392242,45.57251747977179
-2021,Norte,-1.692857245717572,-59.174438146127,28.921055958838515,1991.0590574213986,63.513531661756986,37.04032788071751,20.184383055195717,2021-01-01,1.109638927386694,-107.64529818241112,54.37746855489891,42.572825945476765
-2022,Norte,-4.6074832345612275,-58.35587968001102,29.19883947437903,2010.4578620344562,67.67654324493338,36.56360978069221,24.546515194247434,2022-01-01,1.4073607609261494,-88.24649356935356,44.82692716513986,45.40365479045525
-2023,Norte,-2.815366141467696,-56.2064140346921,28.445151088271928,2155.7122226218917,72.69018283232984,34.07297010373576,23.503866065373547,2023-01-01,0.6337340568201064,57.007867018081924,28.820800537451014,46.349123983664725
-2000,Nordeste,-8.719008132264968,-38.34916730202394,25.652767130398278,821.4093744130204,61.13587392731214,33.4149585231346,21.49486369721368,2000-01-01,-1.564852935849725,64.72587577960462,33.145364357727175,40.927296237029346
-2001,Nordeste,-9.622699519820594,-38.37298072799872,25.46949743182029,668.9043829983665,62.94634174890866,34.42270103010968,19.243564070852912,2001-01-01,-1.7481226344277125,-87.77911563504927,44.76361913473849,41.27841622985224
-2002,Nordeste,-9.208122250357276,-37.44504220047996,25.509769603094252,804.2746143242485,60.83405182607187,35.31432238404787,24.74308347742849,2002-01-01,-1.7078504631537506,47.591115690832794,24.64948307699327,42.350842488871656
-2003,Nordeste,-9.493000934658832,-39.95799231673448,25.675731220325087,818.9665205815793,62.800675044757696,35.68803212747068,18.357108548426563,2003-01-01,-1.5418888459229159,62.28302194816354,31.912455397043225,41.33381222067226
-2004,Nordeste,-9.589364756944212,-38.63609405741007,26.24069549609067,685.5272998581476,73.09503196380233,31.208102838062363,20.076432765736467,2004-01-01,-0.9769245701573332,-71.1561987752682,36.066561672712766,44.62337346666058
-2005,Nordeste,-8.150397902978975,-40.62053351318691,26.6133868370639,782.7638149423789,51.31584795008474,32.59186709353549,20.957133328485227,2005-01-01,-0.6042332291841035,26.08031630896312,13.342274769073612,36.59103930664011
-2006,Nordeste,-8.642984514034953,-39.35166729502311,26.69644926220671,766.6904370619912,63.21390964752474,31.963152855938954,23.317329186827884,2006-01-01,-0.5211708040412937,10.006938428575495,5.264054616308394,41.86970847183995
-2007,Nordeste,-9.692909595260653,-40.26028610873537,27.148483862465095,640.0507963038942,50.84946412231261,31.25855892615692,17.302188741119384,2007-01-01,-0.0691362037829073,-116.63270232952152,58.35091926665221,34.908009949107935
-2008,Nordeste,-8.100400124566749,-39.80600807001456,26.781714178605007,783.60482299135,55.683818931922396,35.748299784910024,22.23937098257079,2008-01-01,-0.43590588764299554,26.92132435793428,13.678615122788639,39.6698288030132
-2009,Nordeste,-8.69270047912339,-38.80968594912617,27.76777262020743,797.3828004629104,62.61866602831509,32.67952863931322,19.67264003508265,2009-01-01,0.5501525539594283,40.69930182949463,20.62472719172703,40.7531170136448
-2010,Nordeste,-8.18713788116104,-41.63644136646695,26.85859720440505,843.0877330131798,56.62574792156427,28.999971221764692,17.428287045730183,2010-01-01,-0.35902286184295207,86.40423437976403,43.3816286208035,36.57877664887417
-2011,Nordeste,-8.370371158076388,-37.44504220047996,28.771231934736377,833.6410747687077,50.04605871904913,33.009636937587814,20.054641731135238,2011-01-01,1.5536118684883746,76.9575761352919,39.25559400189014,35.93770708823657
-2012,Nordeste,-9.828995010922073,-42.012034762999406,27.636269973876395,622.1112735100129,50.98869884500451,35.07039935054701,19.267684608538246,2012-01-01,0.41864990762839227,-172.71130499434219,86.56497745098528,36.69690472572738
-2013,Nordeste,-9.56018104019697,-42.42837722557547,26.868314161802402,622.1112735100129,57.09364091858642,32.54319139066751,17.408261897856324,2013-01-01,-0.3493059044456004,-135.24861132058015,67.79895861251288,37.82289235399172
-2014,Nordeste,-8.252706394876737,-37.683778252999865,26.861812327250288,760.1991788947789,49.17425379793606,27.09169485070067,18.487430256936563,2014-01-01,-0.355807738997715,3.5156802613631726,1.9357440001804438,33.343439051465595
-2015,Nordeste,-8.389629734566535,-38.416674612074125,27.91557040566765,753.5525081346991,53.038814687040315,28.81414054991967,17.31632932801561,2015-01-01,0.6979503394196485,-3.130990498716642,1.9144704190681452,35.05466683819671
-2016,Nordeste,-9.020901593964147,-38.75176036589569,27.613748415761012,747.1569425436483,51.3148157212582,31.968604907068897,18.053121647024234,2016-01-01,0.3961283495130097,-9.526556089767496,4.961342219640253,35.53244425473122
-2017,Nordeste,-8.882672616691218,-38.74330898147144,28.29796551384656,1074.4035838480809,46.95938956631874,27.671617590501572,18.88947874034644,2017-01-01,1.0803454475985568,233.38919629027632,146.5699995642914,32.7520847257819
-2018,Nordeste,-7.7223351042115755,-40.02449354569383,28.275246922570215,739.6977467214906,60.067546533583155,29.626769291075068,20.47638145837766,2018-01-01,1.0576268563222122,-16.98575191192515,9.02168938412368,39.05796383826908
-2019,Nordeste,-9.59157138883583,-41.794508742971665,28.069346130666748,789.643520539799,51.647855981155274,29.94057245468476,14.831843164482635,2019-01-01,0.8517260644187452,32.960021906383304,16.905873985401023,34.011668307832295
-2020,Nordeste,-8.452902618829963,-39.848390883612545,27.72670637785461,764.9653937406246,37.48298271888588,26.1896173177134,21.048675444964704,2020-01-01,0.5090863116066089,8.281895107208811,4.39549070940771,29.164680916357785
-2021,Nordeste,-9.202192652433894,-41.354323423024226,27.44761340431892,728.1826033914493,47.67140200862975,27.576601461789444,14.831843164482635,2021-01-01,0.2299933380709156,-28.50089524196642,14.365444290018667,31.126810301405662
-2022,Nordeste,-9.21768120322722,-38.049760533164495,28.210351898325005,624.9948146664002,42.56063289354937,23.47712378229852,15.957299375146224,2022-01-01,0.992731832077002,-131.68868396701555,66.34070789954627,28.239368503798605
-2023,Nordeste,-7.90122314801281,-40.29411476300428,29.113839276594085,725.8969220493267,49.26216667398112,24.95122471733142,17.176634412969154,2023-01-01,1.8507194927950394,-30.78657658408906,16.341397897217572,32.34322440868262
-2000,Centro-Oeste,-15.107030359954559,-56.25583518296153,23.704701681924494,1451.9346514241172,71.20433669223151,40.240591263423795,25.461186331110884,2000-01-01,-1.7556793007772848,106.67435322115466,54.21590253664895,48.192267955253
-2001,Centro-Oeste,-16.035242322419375,-57.91108088120085,24.128907625442984,1546.752152169823,74.88240371498974,40.68105158116097,27.174214946374533,2001-01-01,-1.3332459086247432,201.4918539668604,101.41254993774257,50.30954144425655
-2002,Centro-Oeste,-15.553649305347182,-59.21289264051514,24.69123713343494,1376.0805068934446,74.96964944410318,40.72757758226473,28.482974973757276,2002-01-01,-0.7709164006327889,30.820208690482104,15.795562545557447,50.75102554444788
-2003,Centro-Oeste,-16.19787789258885,-55.593072728265554,24.226982659621964,1420.60595481859,81.77215854390552,36.7072375554547,25.36558086518738,2003-01-01,-1.235170874445764,75.34565661562738,38.290413745036574,51.33070894375483
-2004,Centro-Oeste,-13.035274867083611,-57.51270149056861,24.19362428439927,1442.927442649372,76.27385832136503,35.54046731070267,28.213752830273016,2004-01-01,-1.2685292496684575,97.66714444640957,49.46783684803901,49.635809370838714
-2005,Centro-Oeste,-14.96473644802827,-58.844507419195345,24.95490797906906,1327.2692571592424,67.76765563473147,40.81622320901441,21.93544925748326,2005-01-01,-0.5072455549986685,-17.991041043720088,9.249143299359378,45.932563993841896
-2006,Centro-Oeste,-15.699725507992586,-57.293145768485054,25.07741472527393,1383.278813398433,65.29053533891039,38.57767150222005,21.028017729985343,2006-01-01,-0.3847388087937986,38.0185151954704,19.2016270021321,43.997920905225776
-2007,Centro-Oeste,-14.786020089265778,-58.16309600722879,24.7480080078208,1355.607066165136,71.37263933579341,36.06824431819123,24.062817645685023,2007-01-01,-0.7141455262469272,10.346767962173544,5.530456744210236,46.58837432348024
-2008,Centro-Oeste,-15.112328049690829,-52.62571672985487,24.891290060107266,1357.593696811405,61.44231222576635,40.31969525539287,24.681174376139484,2008-01-01,-0.5708634739604612,12.333398608442394,6.452131041201428,44.077185779766246
-2009,Centro-Oeste,-15.220969599533223,-54.2367204861011,25.434882136327623,1264.0033694405404,76.33120687275003,37.10091448597702,23.58678441685529,2009-01-01,-0.027271397740104675,-81.25692876242215,40.64210008008113,48.738792419949704
-2010,Centro-Oeste,-14.385833299956575,-56.01594528263323,24.54854069944943,1337.2336261215464,72.20154842969339,38.880333498562436,19.256420147746333,2010-01-01,-0.9136128346182986,-8.026672081416109,4.470142458017204,46.32164546576999
-2011,Centro-Oeste,-14.242492289952695,-53.04011172221995,24.694652170156672,1378.0606986897624,63.08890434817213,36.308950006298474,22.952194703583118,2011-01-01,-0.7675013639110553,32.800400486799845,16.783950925355448,43.01390515223333
-2012,Centro-Oeste,-15.530501147610527,-55.845263384704765,25.229803032465927,1466.8534912142868,55.99954474584995,39.947582806285716,20.2606262163816,2012-01-01,-0.23235050160180037,121.59319301132427,60.91277175646304,40.46228060514018
-2013,Centro-Oeste,-15.57581824064468,-57.72256840265653,25.654540404625063,1411.1444739128685,70.46501404474012,38.988942554451626,23.94804074983399,2013-01-01,0.1923868705573355,65.884175709906,33.038281290231666,47.06710060918173
-2014,Centro-Oeste,-15.275051697151644,-52.95375184546069,26.090892365445757,1524.0138979250687,62.25338773025865,34.632506485650545,23.08102257915022,2014-01-01,0.6287388313780298,178.75359972210617,89.6911692767421,42.21541381154369
-2015,Centro-Oeste,-17.301921164735585,-54.92217991263068,26.91130068669147,1225.4391567546734,68.14560329899774,36.72575059756088,20.20719694908442,2015-01-01,1.4491471526237412,-119.82114144828915,60.635144300456446,44.338125583592685
-2016,Centro-Oeste,-16.515191062198554,-58.074492308652914,26.662421860877732,1382.8842375851114,53.114818227767294,35.72553450786595,20.79835312184835,2016-01-01,1.2002683268100043,37.62393938214882,19.412103854479412,38.20309358002121
-2017,Centro-Oeste,-13.633125732555476,-56.38067735616722,26.173602099413248,1307.464635356444,57.220516189578966,36.82421963680383,19.446790347526445,2017-01-01,0.7114485653455205,-37.79566284651855,19.253555705932037,39.76950947113067
-2018,Centro-Oeste,-13.355032286498716,-57.751236506769516,26.37989462308447,1501.5889889739412,59.37404458554744,31.8092835136224,21.178989631540613,2018-01-01,0.9177410890167437,156.3286907709787,78.62321592999773,39.64609977776788
-2019,Centro-Oeste,-15.249036039556378,-58.76559946192867,25.911525859478182,1195.257127986224,58.71316383838893,34.76340726782296,22.953762901872043,2019-01-01,0.44937232541045447,-150.00317021673845,75.22627127107445,40.80041658626408
-2020,Centro-Oeste,-14.423443036944233,-54.14764490493672,26.62367472710022,1185.5930331732266,55.3583682947777,35.16032723456874,18.811156602981086,2020-01-01,1.1615211930324918,-159.66726502973597,80.41439311138423,38.33479246917603
-2021,Centro-Oeste,-14.688749845456464,-52.18116671905974,26.583326555778353,1203.1042137164673,59.853380096520105,29.17817320106682,21.52983216147047,2021-01-01,1.121173021710625,-142.15608448649527,71.63862875410295,39.153753647369236
-2022,Centro-Oeste,-11.921119191544761,-58.79713514763828,27.07189200576807,1044.132166699454,50.53146307047833,29.981309061624973,19.20035432847251,2022-01-01,1.609738471700343,-242.9035431836978,146.5699995642914,34.967084245220576
-2023,Centro-Oeste,-13.880425088565424,-54.87406152661886,26.503661433868608,1197.4244978319239,54.288102574893536,27.9215356444798,18.414254335475313,2023-01-01,1.0415078998008802,-147.83580037103866,74.43865413541977,35.61597802394395
-2000,Sudeste,-18.406813373360603,-44.46186906041736,21.709614458526904,1325.9722501721483,75.83891030829903,38.47122911621713,25.513977233290703,2000-01-01,-1.5624894967609144,125.48768705271368,63.5250882747373,49.53112602817196
-2001,Sudeste,-20.511215676431185,-42.607587270373166,21.929463900175968,1203.0465983591243,70.40253899911477,39.19390430081741,21.89479978642736,2001-01-01,-1.3426400551118505,2.5620352396897488,1.9523376474007996,46.48762682581933
-2002,Sudeste,-20.98960482025858,-40.30208781010931,21.988041906326245,1350.8157676168678,69.9488828755934,34.34717533452727,25.21747270977654,2002-01-01,-1.284062048961573,150.3312044974332,75.80763327319738,45.848947563528505
-2003,Sudeste,-20.12578692009965,-41.74686994090449,21.94682292949646,1114.2827763817997,65.33191951721376,39.244029825544295,25.92866702195773,2003-01-01,-1.3252810257913588,-86.20178673763485,43.7635338817131,45.684576861136115
-2004,Sudeste,-19.944275087711304,-44.53777738110966,22.61606949850603,1267.305063613529,64.08370801007231,37.28568846716274,23.783391545577835,2004-01-01,-0.6560344567817893,66.8205004940944,33.73826747543809,43.9542072078511
-2005,Sudeste,-18.90580848152905,-46.21305181748334,22.210125582327294,1146.2896843715803,63.48938297394538,35.43029373741579,26.449243799029176,2005-01-01,-1.0619783729605246,-54.19487874785432,27.62842856040742,43.959614450511644
-2006,Sudeste,-21.69246462971482,-38.85328039350028,22.992708878076602,1195.6859544371753,66.75486940754365,37.17860721047344,20.887204210488505,2006-01-01,-0.27939507721121615,-4.798608682259328,2.539001879735272,44.12169118930604
-2007,Sudeste,-18.470449680539385,-43.88156313197102,22.922031047374,1259.087693218598,61.80812107415955,40.106603106913134,19.684603636855428,2007-01-01,-0.3500729079138196,58.60313009916331,29.476601503538564,42.66061045279439
-2008,Sudeste,-20.15800789857899,-43.97214141617411,22.930544215358417,1161.7847504281265,64.49235254535162,35.58112381446482,18.355602980452314,2008-01-01,-0.3415597399294015,-38.699812691308125,19.520686215618763,41.97795905661579
-2009,Sudeste,-20.42688106994742,-44.04825017422001,22.651113976688087,1202.8487934031045,72.55091703670544,35.36600638999197,24.64426253602184,2009-01-01,-0.6209899785997308,2.36423028366994,1.5727635858421642,47.02344749248632
-2010,Sudeste,-21.01210437526017,-43.60383047846429,22.971328506006827,1335.4118896170962,65.65997134324542,33.603017331354295,22.7000356605342,2010-01-01,-0.30077544928099087,134.92732649766162,67.61405097347131,43.15490443486472
-2011,Sudeste,-21.65485667186577,-44.28872082384788,23.89236517027806,1170.5716826129026,58.80466494785645,32.47422430454868,21.019265440787784,2011-01-01,0.6202612149902436,-29.91288050653202,15.266570860761131,39.569912902743525
-2012,Sudeste,-19.176829416038085,-45.147324013760475,23.86141987779202,1305.3083416412967,65.57128700212407,35.48675858961023,21.429502470030947,2012-01-01,0.589315922504202,104.8237785218621,52.70654722218315,43.30339311874198
-2013,Sudeste,-19.926682032811595,-45.09371788248078,23.096075548773044,1102.246705838586,56.65527530880529,31.766093217747486,18.35771755757716,2013-01-01,-0.17602840651477436,-98.23785728084863,49.2069428436817,37.699253356119506
-2014,Sudeste,-21.289960899741054,-44.065506540432,23.86394720934449,1261.6760699654412,47.635464280632476,33.27356725704748,25.239330046190176,2014-01-01,0.5918432540566734,61.19150684600663,30.89167505003165,36.608054903224286
-2015,Sudeste,-21.295078772063615,-45.08684954226753,24.376919230544814,1346.3307751092684,51.916090184543414,31.82729380212194,20.345398885689583,2015-01-01,1.1048152752569962,145.84621198983382,73.47551363254541,36.41824388016082
-2016,Sudeste,-20.335784699290127,-45.42569156535433,23.046182918283698,948.4877239003146,46.732996916609856,33.39274226018844,20.044876545731523,2016-01-01,-0.22592103700412025,-242.9035431836978,126.11138012806208,34.72448440841993
-2017,Sudeste,-18.33097847471061,-43.78713954461621,24.495581332819267,1109.440909235561,53.45982388414129,32.75428073231303,21.016750639101733,2017-01-01,1.2234773775314487,-91.0436538838735,46.13356563070248,37.51523896508095
-2018,Sudeste,-20.259591351363603,-44.50995443484171,23.89373009245858,1240.315908370228,54.439917982904284,33.81421032329841,20.266366289560384,2018-01-01,0.621626137170761,39.83134525079345,20.226485693982106,38.000140177019354
-2019,Sudeste,-21.503142953118108,-40.992014022834624,24.75540267941677,1155.78241791744,61.86044743094163,30.371271550116592,19.49124630942783,2019-01-01,1.4832987241289501,-44.702145201994654,23.092721963061802,39.702934330239984
-2020,Sudeste,-20.245743064085943,-49.30193961678602,24.076827114770143,1206.679804728435,54.24333252099473,30.503728390282824,17.20165978476155,2020-01-01,0.8047231594823252,6.1952416090005045,3.499982384241415,36.008949460911204
-2021,Sudeste,-20.27272356974767,-41.81698629615508,23.573592478731726,1102.6449595892875,50.643627790738286,28.73812474234843,19.464675844706772,2021-01-01,0.30148852344390775,-97.83960353014709,49.0705460267955,34.71829129241188
-2022,Sudeste,-22.69688664294157,-41.507829615004745,23.80173540683532,1165.1807178777246,55.01659312330875,29.4592395668043,22.024918558167457,2022-01-01,0.5296314515475018,-35.30384524170995,17.916738346628726,37.45188468681503
-2023,Sudeste,-20.05429486651781,-48.1467804648163,24.92885096799683,1134.432276460792,38.944323454960575,30.754600986089702,20.690840092309955,2023-01-01,1.6567470127090118,-66.05228665864252,33.85451683567577,31.01136170550413
-2000,Sul,-26.728421162804626,-50.46589946826148,19.59936992202428,1632.6133022242118,61.5390088444603,33.03531804093824,28.746065884424464,2000-01-01,-1.6094796630055797,114.9297261766933,58.26960291984943,43.15001871539293
-2001,Sul,-28.23956708909202,-49.22073840875313,19.698874610837308,1468.366903231035,50.57959281932557,30.200686286736545,30.84569404368733,2001-01-01,-1.5049924957200638,-49.31667281648356,25.41083265610181,39.30125127898413
-2002,Sul,-28.081056540408262,-50.83543202144915,19.59936992202428,1679.3592236598206,51.519942454688824,31.677881355041112,30.84569404368733,2002-01-01,-1.7556793007772848,161.6756476123021,81.8402717374103,39.37947081053071
-2003,Sul,-25.946847146667096,-48.869039249869296,19.698527378749823,1561.944568964397,61.10762481793966,29.44575587442379,28.09245775255929,2003-01-01,-1.5053397278075487,44.26099291687842,22.883166322342987,41.70451401527079
-2004,Sul,-27.039555153856654,-52.03457690020075,19.620611014459634,1521.6407414263567,57.37606632107449,29.38566004619002,30.221337217241476,2004-01-01,-1.5832560920977379,3.9571653788382264,2.770210735467982,40.83252570745925
-2005,Sul,-26.318499302627377,-48.18130511962884,21.460057918228088,1672.292226931515,54.73225723128008,27.447547371520947,30.6407610731665,2005-01-01,0.25619081167071656,154.60865088399646,77.43242084783358,39.319395425918266
-2006,Sul,-26.97168162386954,-46.4022037527615,21.091841818497173,1490.4351961631153,54.874778360979136,28.066576986039063,29.678402533130047,2006-01-01,-0.11202528806019885,-27.24837988440322,13.68020258623171,39.273405200142385
-2007,Sul,-26.970243860504258,-51.72567712087932,21.415106970039975,1413.5016964506005,55.65095200506922,26.861848180071682,25.03346360369735,2007-01-01,0.2112398634826036,-104.18187959691795,52.19655973020028,37.8289743371584
-2008,Sul,-26.061716194024,-51.891005042801545,21.206916281904203,1392.1631696041313,50.34467985932275,27.26564479839316,26.458769126611134,2008-01-01,0.0030491753468311344,-125.52040644338717,62.761727809367,36.25519612123039
-2009,Sul,-27.516044728217373,-48.0932310457646,20.218221111182704,1601.905342960651,48.28238941646669,30.976848810606985,27.15713664194047,2009-01-01,-0.9856459953746679,84.22176691313257,42.60370645425362,36.753151402350916
-2010,Sul,-26.90387922305902,-47.84085570853858,20.70267914457996,1406.739673446024,52.28357410157536,26.622941352770894,27.242437489625992,2010-01-01,-0.5011879619774113,-110.94390260149453,55.72254528173597,37.07304329334921
-2011,Sul,-27.462275288705044,-52.045720054306535,21.432755171519357,1703.740287763654,45.5429187206205,33.438677173153046,28.37177897771005,2011-01-01,0.22888806496198555,186.0567117161354,93.14279989054869,36.76030433350713
-2012,Sul,-27.434496227432316,-51.840373634191714,20.527592879882224,1313.5461896464103,51.6064811698767,28.812722758283517,27.224967219257813,2012-01-01,-0.6762742266751474,-204.1373864011082,102.40683031389167,37.45389946121308
-2013,Sul,-27.309172123468638,-51.56356921772102,21.810407753755605,1684.8630281333517,40.182734152723846,27.847933116954966,23.938006934753712,2013-01-01,0.6065406471982335,167.1794520858332,83.89299636651572,31.608875676602143
-2014,Sul,-26.77786622836629,-53.688901022068556,21.360152587192076,1529.7973988941749,52.97186514314996,25.413719222246268,27.10822178896845,2014-01-01,0.15628548063470404,12.113822846656376,6.13505416364554,36.9453283606244
-2015,Sul,-27.478748621663478,-52.83730389296839,21.40119447199198,1492.502601856439,49.31950915119565,28.183228454470154,27.308128012306774,2015-01-01,0.19732736543461016,-25.180974191079486,12.689150778257048,36.37521060051134
-2016,Sul,-25.74424387442648,-53.00828153350414,22.174402067878617,1441.1602652321485,47.36710519330851,25.283506956220105,26.945667852383817,2016-01-01,0.9705349613212455,-76.52331081537,38.74692288834562,34.61559451990458
-2017,Sul,-27.894607302219505,-52.53559513020826,22.136123204282928,1529.0440462178158,50.1308462240754,26.330976151029855,27.72677884241359,2017-01-01,0.9322560977255563,11.360470170297276,6.146363134011416,36.26966498766319
-2018,Sul,-27.18687164416136,-51.06936977477356,21.77472464833464,1478.8452254096892,52.675198309269405,30.8592995113202,26.972192135184766,2018-01-01,0.5708575417772686,-38.83835063782931,19.70460408980329,38.419526817659246
-2019,Sul,-27.439731058274177,-50.531570534926956,22.80864136027942,1586.4171447233914,48.55011119465587,25.199184548376785,25.96183841932644,2019-01-01,1.6047742537220486,68.7335686758729,35.16917146479747,34.76835136817331
-2020,Sul,-25.553022115646268,-47.898999014371846,21.586144612135715,1480.9879822575083,35.74817565348264,27.342024913440916,24.642277272734876,2020-01-01,0.38227750557834383,-36.69559379001021,18.538935647794275,28.957020454059016
-2021,Sul,-26.803445223488424,-52.99670808147758,22.726083103296993,1478.0736544070573,35.74817565348264,23.47712378229852,21.1998172634028,2021-01-01,1.5222159967396216,-39.60992164046115,20.566068818600385,28.239368503798605
-2022,Sul,-25.968155460531364,-49.031355203046836,22.83412162434625,1420.160517909049,37.74547232891291,24.2778491059626,25.12010600348256,2022-01-01,1.6302545177888774,-97.52305813846942,49.57665632812915,29.917575464398713
-2023,Sul,-28.23956708909202,-51.427977688451016,22.414272136411608,1444.3054376278933,40.130455251054165,27.54160127127102,24.41537537432622,2023-01-01,1.2104050298542361,-73.3781384196252,37.294271724739716,31.63927509410084
+year,region,latitude,longitude,temperature,precipitation,bee_abundance,species_diversity,honey_production,temperature_anomaly,precipitation_anomaly,climate_stress_index,bee_health_index
+2000,Center-West,-15.671140277396582,-56.30504506490472,23.239233237728687,1200.6939628945845,74.34456674656137,42.152480314932774,26.44615948525055,-2.3165159294960915,-97.15140303583848,1.471601616434712,50.31741863867955
+2001,Center-West,-14.687990528448756,-55.23321227129301,23.845257356419527,1263.8310280620892,64.97590565337106,37.960851847508785,28.605559447994956,-1.7104918108052516,-34.01433786833377,0.9105603020901745,45.96028564999955
+2002,Center-West,-13.844687963893303,-54.00035150617941,24.70726022447976,1238.7379819948485,77.90235452305981,39.970488571323465,25.661301225911636,-0.8484889427450177,-59.10738393557449,0.6488033766466732,50.85047874839446
+2003,Center-West,-14.39123852777556,-58.11707216395682,24.87973009699201,1147.0731706238653,69.36630078378244,38.904796043222724,26.37894549500742,-0.6760190702327691,-150.7721953065577,1.0008227901305744,47.33164277498202
+2004,Center-West,-17.2912895085981,-56.25001806063916,24.852721029336262,1306.0865994627432,68.85223793856701,39.236512075089024,24.84295535034505,-0.7030281378885164,8.241233532320166,0.34746344317336625,46.76473540305703
+2005,Center-West,-14.69563327096974,-53.03708890468205,23.961036121838493,1442.3346943206,65.30341185659503,36.59413195331858,24.354837011584635,-1.5947130453862854,144.48932839017698,1.37528914278691,44.40605543210898
+2006,Center-West,-14.927966426767423,-57.487176457625125,23.54437272062911,1332.3811585791339,66.51119231221439,34.73390849164293,22.87581781524481,-2.011376446595669,34.53579264871087,1.0452407924022586,43.88739481695208
+2007,Center-West,-14.586109714627307,-57.64450003458064,25.084786064277708,1374.1268342811418,71.05249568173546,33.419250519321764,25.03451576139337,-0.47096310294707067,76.2814683507188,0.5630269260246412,45.95712815690872
+2008,Center-West,-13.383790318348792,-55.59538761633761,24.89084742329526,1456.747836627601,67.09725655618753,34.98262307836861,22.209969099889335,-0.6649017439295193,158.90247069717793,1.033882626421183,43.9966802759524
+2009,Center-West,-17.063238276804743,-54.31122961902936,25.57128363694301,1474.072961052856,68.81722305489068,35.95221464751264,24.983344385670335,0.015534469718232913,176.22759512243306,0.8293296522610837,45.80755693191116
+2010,Center-West,-15.591103425191106,-55.97714788887868,25.83856348008059,1327.8688152230384,68.11395709573117,35.935355957137254,25.561878356717905,0.2828143128558125,30.023449292615396,0.26443189421816066,45.694753132449016
+2011,Center-West,-14.409093677206645,-53.34207881912602,25.622075294017815,1463.6194585221572,72.03809088633649,40.04837536833729,23.863497597083157,0.06632612679303662,165.7740925917342,0.8028645996497819,47.98879824416073
+2012,Center-West,-16.58159439945394,-54.28641203486695,26.116949416442903,1330.9517962781933,62.85128663698245,38.03807369633634,22.297067750279922,0.5612002492181247,33.106430347770356,0.40117894957221634,43.24105708877786
+2013,Center-West,-13.52405095194649,-54.31635986621988,25.920135520121214,1231.4775689216015,56.31136308968439,33.55407863688196,23.516626810791305,0.3643863528964353,-66.36779700882153,0.46991385305787475,39.64575687017574
+2014,Center-West,-14.631643387299341,-54.891766997351816,26.222386649447834,1249.2556169540394,68.16219903938166,35.82585168751061,21.730479975863698,0.6666374822230559,-48.58974897638359,0.5197862248843066,44.53177911476496
+2015,Center-West,-14.153416014156596,-51.344693799307834,25.60328299879981,1154.1202617869767,60.29908087864416,33.96845222295152,23.958050772669427,0.04753383157503066,-143.7251041434463,0.6916962992813757,41.49758325014395
+2016,Center-West,-15.570943661521795,-56.41032337773209,26.040059238657644,1206.8387885396783,66.65991874462601,33.86605493261546,18.163274788091492,0.48431007143286564,-91.00657739074472,0.6376193361164272,42.272766414062495
+2017,Center-West,-14.18623631003379,-60.00704458413303,26.387630875385476,1253.3287659751659,62.13427687026018,34.4576078205699,20.894002655067055,0.8318817081606973,-44.51659995525711,0.5734049251398441,41.45919389079516
+2018,Center-West,-13.931528444010961,-52.791491276965715,26.883621433076538,1362.9671975900342,58.692138435939796,34.9774726687334,17.32272383583896,1.3278722658517594,65.12183165961119,0.8875760235134419,39.166914325747626
+2019,Center-West,-14.767121979862832,-56.91539886191365,26.276798774926124,1448.1639396383375,60.222140339796695,32.71278738005805,18.053356600415157,0.721049607701346,150.31857370791454,1.0184977223447935,39.318699330060646
+2020,Center-West,-14.765599107301304,-55.78423911131394,26.94748296213492,1131.3470658536003,67.36375845280541,35.17118132633828,23.51033315527999,1.3917337949101416,-166.49830007682272,1.3887962404468033,44.54995772560765
+2021,Center-West,-14.729656760718274,-53.38089863027895,26.597018463981208,1302.3262250868613,67.12403308005138,30.41177572162824,22.312109094378155,1.041269296756429,4.480859156438328,0.4785786543788129,42.66677867682247
+2022,Center-West,-15.863345264739381,-59.20451908211785,26.80773556916834,1152.4673452146812,56.216470893055195,32.36050421168106,18.82190909864781,1.2519864019435616,-145.37802071574174,1.228799526286481,37.841312350320734
+2023,Center-West,-15.147528680240352,-58.50329442821228,27.497711425214444,1297.4697088463201,55.80095296658538,37.195347453191665,16.994993087918207,1.9419622579896654,-0.3756570841028406,0.8552968381783961,38.57748334896711
+2000,North,-2.695282920245569,-60.85665564432622,26.907348137843137,2108.054771399839,78.386501938228,42.95300501475627,26.37411780937935,-0.9372889766874337,-1.1083396288868244,0.5063340601021705,52.15273762253189
+2001,North,-4.039984106240496,-60.70426710097646,26.662286572128117,2243.1943352749413,79.1442006446013,45.01072518295611,26.908415914423237,-1.1823505424024532,134.03122424621552,1.3565735030004817,53.23342258705433
+2002,North,-2.2495488041935428,-58.9353816288933,26.916949900083555,2201.199095346187,85.25974069883458,44.74434119403048,26.254067271445603,-0.9276872144470154,92.03598431746104,0.9933391680110677,55.40341881917666
+2003,North,-2.059435283608786,-59.269111871271846,27.243990326807474,2249.4833180154724,77.86390907102579,46.67489688961131,26.93580953151506,-0.6006467877230968,140.3202069867466,1.0800915860030977,53.228775554748225
+2004,North,-4.951035188653837,-59.174534776808024,26.112663029825733,2131.1877788446077,79.63092636023784,45.27891814400307,29.08848220070572,-1.731974084704838,22.024667815881912,1.0437519952841157,54.16259064750777
+2005,North,-4.302179506862318,-59.138357993984236,26.916892922359533,2183.245273455497,87.1483154019738,43.97461168351641,27.009749308803734,-0.927744192171037,74.08216242677145,0.89619753167815,56.15463445848556
+2006,North,-2.8721595968327147,-55.71670479825908,26.94821119003302,2223.428604614125,78.00128884034504,45.83964926069999,26.117512934618276,-0.8964259244975494,114.26549358539933,1.096965034114974,52.787664194733495
+2007,North,-3.316242592343582,-60.812830032769234,26.968847204097475,2123.413172114933,82.6006213205963,40.18769128265864,24.102605537826637,-0.8757899104330953,14.250061086207097,0.54463253412558,52.3273375743841
+2008,North,-3.016801157504289,-61.024485458143076,27.308827692742252,2193.503610712263,73.37538986082018,40.93788823884833,22.56367758885216,-0.5358094217883185,84.34049968353702,0.7424998907713475,48.40062569263822
+2009,North,-3.85304392757358,-61.62754545649575,28.49261696065368,2075.111753719422,76.14672525414011,40.01641333458026,24.679558094896816,0.6479798461231105,-34.05135730930397,0.5301961661362752,49.86748153049916
+2010,North,-2.1206020251371713,-58.76804115484901,27.197892634931883,2098.477224039145,71.55423755153404,40.74211952412325,25.283739246189445,-0.6467444795986879,-10.685886989580922,0.403075021906632,48.42945265170743
+2011,North,-2.222208064571052,-57.742055414558216,28.4200974475375,2090.0870801653846,73.73961722919285,38.42054074836564,28.66629460976846,0.5754603330069301,-19.076030863341202,0.4104329931662869,49.62189749911737
+2012,North,-2.933969302438784,-60.22789491530975,26.951147702247912,2002.1551658758785,78.76675807859132,42.096388850150554,25.130114260337077,-0.8934894122826584,-107.00794515284724,1.0561172265643561,51.67465416458282
+2013,North,-1.8727587930319671,-61.68031295392505,27.881677677660708,2163.914639382906,65.05905005795131,40.033815475367675,26.57417467422003,0.03704056313013737,54.75152835418021,0.31610613774241403,46.006017068056835
+2014,North,-2.5324906577479545,-61.64896243138248,28.30199966514996,2061.7554181536407,74.99820517425103,37.18882219360338,23.34923488987822,0.4573625506193899,-47.407692875085104,0.5007314455677615,48.16069919474489
+2015,North,-3.8592924628832384,-58.6988144243506,28.677820355337307,2103.423324916247,73.14519975079183,37.75145053323709,21.71706898890508,0.8331832408067363,-5.739786112478669,0.47582817963321555,47.09863575695938
+2016,North,-2.631249215917501,-58.513491657593114,28.874562034832234,2143.7268398036344,68.11620674325515,40.06181030359605,21.895853432056995,1.0299249203016636,34.56372877490867,0.7368558509231222,45.83378181799797
+2017,North,-3.958882600828999,-58.91369146338961,29.045573558511258,2133.783552385603,62.98710203240555,40.763209657527895,22.11476526223284,1.2009364439806873,24.62044135687711,0.774327341844997,44.05823328889044
+2018,North,-2.1215496986927276,-61.33101941457739,28.482069304477026,2149.1472065494527,69.70847362552587,42.73259221816618,27.561287290957285,0.6374321899464555,39.984095520726896,0.5566756602500023,48.971553302947385
+2019,North,-3.049925910986253,-59.53567735386656,28.53563240266213,2066.2384080707966,65.8307973242462,44.21902927984075,21.400704902420795,0.6909952881315604,-42.924702957929185,0.601183787078632,46.018239184376945
+2020,North,-3.1848623635452604,-59.76662838171854,29.449568137449944,2027.2353861868892,68.77207670894742,38.872297127421554,24.45958710312486,1.6049310229193736,-81.9277248418366,1.3001502410377286,46.508395952742895
+2021,North,-3.6809295444039414,-59.562622806541974,28.941739144201467,2055.0716571702296,66.84839116239982,35.71657593384932,20.71588477426073,1.0971020296708964,-54.09145385849615,0.8784062834965625,43.66909467739294
+2022,North,-1.7774586613259697,-58.25714244410362,28.412849075562466,1887.788295734632,73.87845967399589,33.083733525493905,24.12401559096588,0.5682119610318956,-221.37481529409388,1.5014719937153314,46.713708604536286
+2023,North,-3.1545294820688023,-59.552808902450636,28.620027671597914,1905.2887527576913,63.80322186263488,37.53542292468767,22.769901932211727,0.7753905570673432,-203.87435827103445,1.5173479592923749,43.612886202123775
+2000,Northeast,-9.15663789765809,-40.76637464227198,26.122869262202357,712.3139221832412,67.3576926127257,34.41565762839289,25.487869052335583,-1.0243949653968691,-4.723309522141449,0.6053731318744153,44.91413504930882
+2001,Northeast,-9.040762526135435,-37.077111415515596,26.72417799490043,784.0520054443095,69.19401387170785,34.44519737691156,22.617116752259207,-0.42308623269879675,67.01477373892692,0.5022496210615597,44.79629978743437
+2002,Northeast,-9.654787695429391,-42.214091364086975,25.950774853319626,611.1836826034672,64.03801620355972,33.800393069673184,23.134237223159335,-1.1964893742796008,-105.8535491019154,1.0960340655402283,42.69559556927365
+2003,Northeast,-8.55392779851792,-41.78945403791165,25.747651824301926,633.7302580695966,64.1500863898221,34.26164676333538,20.624249397278412,-1.3996124032973007,-83.306973635786,1.1250373803799008,42.12580340411298
+2004,Northeast,-9.45498348034078,-38.71334641062191,26.152641231610833,986.8377546810907,61.78751586832412,36.55167959816023,21.089613370848845,-0.9946229959883937,269.8005229757081,1.615896887345948,42.00739423803237
+2005,Northeast,-10.225605763767248,-40.78921024571918,26.55419372693665,638.6490462204231,60.65259587850055,28.127552683354796,19.30080452731249,-0.5930705006625772,-78.38818548495954,0.6437542752092602,38.4895455146004
+2006,Northeast,-10.277937574319619,-40.01024373344014,26.636378797680923,651.1910073719273,56.17481852793794,32.4393429492415,22.413950477885358,-0.5108854299183037,-65.84622433345532,0.5480358750318125,38.925915439313236
+2007,Northeast,-8.827412082277881,-40.32688579704902,26.00154106970346,938.0391789190879,60.49551478662741,32.91824223404924,17.81730579724853,-1.145723157895766,221.0019472137053,1.5133195602789367,39.4188703240403
+2008,Northeast,-7.420908743589565,-39.32485090240213,26.828206912454988,1038.3328038805626,57.55704526880211,32.80937928372202,21.477744163096194,-0.31905731514423863,321.2955721751799,1.4283651458802726,39.30895514156631
+2009,Northeast,-8.840008386426561,-37.18503627737257,27.03258917199639,624.1476849435528,67.63975458813124,31.125736054748725,19.27087179803316,-0.11467505560283797,-92.88954676182982,0.425809425983852,42.17488419108707
+2010,Northeast,-9.118638326109883,-39.81883018619651,28.52787572686818,697.9577130168491,64.17112233846316,28.008295565477617,21.733798534760723,1.380611499268955,-19.07951868853354,0.8651672345882884,40.591077165456774
+2011,Northeast,-8.714173860397457,-38.71212241344628,28.26523720155161,762.4164246592684,57.36401537147816,31.829904010906482,20.632172760071214,1.1179729739523836,45.37919295388576,0.8166057036109435,38.684229179884575
+2012,Northeast,-7.693998258293175,-44.10034420206205,26.748923554882097,894.6088948753419,50.74993134283051,34.28078738485011,22.51085753268491,-0.39834067271712925,177.57166316995927,0.9166478810957146,37.33746601239271
+2013,Northeast,-8.780617498636143,-40.097436803860234,27.210178678722666,616.5316834615242,48.133594813995956,27.410538059732943,20.070245034451204,0.06291445112344007,-100.50554824385847,0.4256701462017018,33.49767285385363
+2014,Northeast,-9.410927230833737,-41.68646054058574,26.626282624984754,684.167867579702,58.78180172196607,31.858090810005073,15.203745575650043,-0.5209816026144729,-32.86936412568059,0.4259853089136162,37.63127160448296
+2015,Northeast,-7.893711289940111,-42.437626120847256,27.271662748143648,779.9076706496527,53.27026786486613,29.125558249406385,17.953050175942728,0.12439852054442113,62.87043894427006,0.31501114793333934,35.43168967355118
+2016,Northeast,-8.571243561538386,-41.756304733857505,27.937189089110728,739.1287813597054,53.48767353869719,29.312133917037958,15.482072571332107,0.7899248615115013,22.091549654322762,0.5383312671441705,34.8333313619899
+2017,Northeast,-7.464244008004008,-40.66824688140162,28.593077389924243,651.5148276595988,41.494461369476866,26.981159500331234,16.5286577065718,1.4458131623250168,-65.52240404578379,1.08257142800215,29.650729709861658
+2018,Northeast,-8.816765562778095,-38.16819491528798,27.55385404517992,669.2263992005697,53.98501849474743,28.070218090070856,20.3268046926404,0.406589817580695,-47.81083250481288,0.4183507384699673,36.11311423271235
+2019,Northeast,-10.224469031720501,-42.65278543547913,27.720555614220192,538.5973756847409,50.750362560637036,30.991393537224003,18.318545882664957,0.5732913866209657,-178.43985602064174,1.0202746937884384,35.0931268502215
+2020,Northeast,-10.368159199245666,-39.93873701481117,26.94640919085246,645.7478429938378,42.05654223010153,29.20878874484498,15.384747838494679,-0.2008550367467663,-71.2893887115448,0.39146354562468116,30.200677867042508
+2021,Northeast,-7.34907206776875,-40.96833886666716,28.258922186803492,636.404729259974,44.38150639753287,31.16027538473293,14.494304397553984,1.1116579592042655,-80.63250244540859,0.9496479593486007,31.44897649369922
+2022,Northeast,-7.276334279216703,-40.655346188723925,27.840812222449483,679.7387280930603,47.188151239173905,29.702649864911912,16.326433750969912,0.6935479948502561,-37.29850361232229,0.5420499024006135,32.68398558043411
+2023,Northeast,-9.179519213282601,-37.99448434939079,28.282336343580358,594.4672781180984,45.26233399178122,27.878173357859154,13.566879701237745,1.1350721159811314,-122.5699535872842,1.1256389088653207,30.53844951444156
+2000,South,-27.713874824036484,-55.64071202477229,19.742292637846003,1609.90348210072,53.6007952117999,33.02244362909507,29.19601483974159,-1.3305330899556083,51.116329897005926,0.844023502577478,40.105855625370964
+2001,South,-28.316830211058242,-52.92727688873761,20.598550444844363,1686.5620835046182,67.53951787911282,32.434703232909946,30.227339212739313,-0.4742752829572474,127.77493130090397,0.7875895677292488,45.8144198853399
+2002,South,-26.164192103685664,-52.83031223169185,18.675035553646413,1766.715973982562,66.9058462696459,27.7309328033508,30.024668915269466,-2.3977901741551975,207.9288217788478,2.0355673641414858,44.089019023444436
+2003,South,-26.65064937743957,-51.40220930903766,19.267234177362354,1632.0645398188408,64.18740734930415,30.71115246414018,26.93853330216468,-1.8055915504392566,73.2773876151266,1.1627496277797484,42.96986866961312
+2004,South,-24.617397717315928,-48.774068216679474,19.733224656601973,1536.7413738196913,57.79780621471416,32.60009842397376,27.994039491399608,-1.3396010711996382,-22.045778384022924,0.7190719128604692,41.297363860497676
+2005,South,-26.579811401275062,-51.490121302381404,21.28540297220611,1478.0782421893398,62.40529951780676,32.37931704301003,31.571206053745968,0.21257724440449977,-80.7089100143744,0.45714214313071644,44.1472767361495
+2006,South,-26.612296858737984,-53.06162744054231,20.699986972360275,1562.0146943844568,54.8428745986613,31.96471716403135,26.110623583227078,-0.37283875544133593,3.227542180742603,0.1872116832812053,39.35975206364205
+2007,South,-27.166927930108564,-51.11390908242706,21.112564257320216,1524.421355698431,54.92702986274976,29.64304380510727,23.9406041455372,0.03973852951860479,-34.36579650528324,0.17110742764066655,38.04590633029325
+2008,South,-26.18322413279921,-48.901652757117304,20.089816492002523,1446.3192963836402,47.57612140855965,27.527300648409526,24.18935317177585,-0.9830092357990878,-112.46785582007396,0.9554645454627264,34.54544470947947
+2009,South,-26.374914798751846,-52.95191763222981,20.22393064875073,1746.1799129124156,54.15886956597246,26.702167562675182,27.710393806196816,-0.8488950790508802,187.39276070870142,1.2261606316790332,37.987316237050585
+2010,South,-25.748274991434805,-52.8211497678429,21.286079534381454,1713.2994489028654,51.606277468209036,29.210948939935495,27.453138903891485,0.21325380657984283,154.5122966991512,0.7853532010203512,37.64173734043171
+2011,South,-27.521322918637285,-49.882902081451554,21.174095033343875,1641.8657379207602,43.921522024523334,28.932473112208587,26.895213071758455,0.10126930554226377,83.07858571704605,0.4160606856887861,34.316914664999445
+2012,South,-27.43540747294396,-51.4430722314616,21.590112856535868,1429.6540906538698,49.30923183222136,30.400749179731868,29.29794487524831,0.5172871287342566,-129.13306154984434,0.8135666142600753,37.633300949382594
+2013,South,-27.47910317091198,-49.70503061858105,21.270006101095102,1600.7193237340757,48.297004354629,29.678220170249386,21.263026785968037,0.19718037329349158,41.93217153036153,0.27772393252409255,34.601175828716826
+2014,South,-26.20919827882101,-51.02729358359059,21.671234061959733,1572.6582931237033,56.81389278974494,26.86597746954635,27.13969383581817,0.5984083341581226,13.871140919989102,0.3390882618462935,38.92725850750733
+2015,South,-25.501625559703267,-49.596672703820865,21.720958407464806,1546.4284754638836,51.42758838169748,24.294196937930433,29.209832925346277,0.6481326796631954,-12.358676739830571,0.3554240382536138,36.622244311662
+2016,South,-27.4588404931493,-53.07015622808128,22.214486520407476,1620.6187006174212,46.14418371719646,25.796569414673858,23.57052542673831,1.1416607926058653,61.83154841370697,0.8040561771923866,33.26780193930224
+2017,South,-27.42477372779309,-51.024169301576535,22.353770186557988,1552.6675490687364,46.46132785335324,26.521418911360183,24.80560436772456,1.2809444587563767,-6.119603134977751,0.6211173036553007,33.982638125066714
+2018,South,-26.6859227761302,-51.42138002872631,21.019381151191027,1556.6101192826075,44.568887977477516,25.3349639440038,23.798563991629717,-0.053444576610583994,-2.177032921106729,0.034452597893471715,32.567613571681065
+2019,South,-27.2457615000129,-53.43178184820826,20.764682345056357,1539.7775848589602,42.46369094558748,24.235516131670774,23.25478616871451,-0.30814338274525355,-19.009567344754032,0.2273318846861908,31.232567068350576
+2020,South,-26.04794634686011,-54.126955895737375,22.835829847286558,1503.6443581915148,37.19655966523248,25.549247890809365,24.130084685606093,1.7630041194849468,-55.142794012199374,1.0624340610350587,29.78242363901763
+2021,South,-29.251777290619046,-49.62850192945068,23.069156795087597,1284.6823652894511,40.983871046747964,23.68773669109182,27.37670969780379,1.9963310672859862,-274.10478691426306,2.143434788792556,31.712882335367873
+2022,South,-27.826705046916896,-51.70190476443596,21.865887057857215,1464.8838468714905,37.13907393412874,23.091693919505776,27.935642916192432,0.793061330055604,-93.90330533222368,0.7849128832526493,30.16383062436096
+2023,South,-27.782416333961446,-53.04456056329466,21.484098756072623,1394.3808041150876,33.74842230538427,24.62290566860688,23.328121549919704,0.41127302827101175,-164.4063480886266,0.9211255231776918,27.88467708771168
+2000,Southeast,-22.96452883784165,-43.48354235335461,22.277850800485197,1433.6186100012171,72.57705201450405,38.94227258679986,24.328781405954604,-1.1149533195315655,222.96316349368226,1.4347497137215373,48.01213700362796
+2001,Southeast,-20.543495507932633,-40.87056395666019,23.442783112073425,1418.2732593755084,66.24274034583046,36.585269218714025,22.95820253585699,0.04997899205666201,207.6178128679735,0.7877690075558038,44.36013766470349
+2002,Southeast,-17.579584987752597,-44.72354095488834,22.291221758238063,1261.704788308834,74.63310564992358,36.481071181654535,27.17391089547731,-1.1015823617787,51.04934180129908,0.7979937446467111,48.94973688310898
+2003,Southeast,-19.56511572853635,-45.882244191849914,22.280967460303604,1316.780216392603,64.2533666656415,39.68180701873712,22.86904583673791,-1.111836659713159,106.12476988506819,1.0053013196116867,44.4666025228991
+2004,Southeast,-20.55957228604949,-44.89712841605671,22.134839973524706,1032.9208468982733,63.3386627149236,34.527523547149045,26.292296961369473,-1.257964146492057,-177.73459960926152,1.3485146279285152,43.58141123852499
+2005,Southeast,-19.534919790499693,-43.095332098713726,22.089338049827212,1151.7565383212927,68.35783196706615,31.43530890335841,20.48817077834903,-1.3034660701895504,-58.898908186242124,0.9387261988019541,42.92017669133869
+2006,Southeast,-21.560958352994444,-47.13151814436103,22.083751710048052,1231.4916732021782,68.59724110733686,39.134917468284655,23.609300942301235,-1.3090524099687109,20.8362266946433,0.8024862554693274,46.26216196611051
+2007,Southeast,-20.297323362697636,-42.72505819469776,22.41889552510493,1147.1345239442705,66.15354029794189,40.66387329651835,22.81271357106072,-0.9739085949118333,-63.52092256326432,0.7728211760955885,45.50439217945048
+2008,Southeast,-19.90052252698426,-45.07754263523549,23.031504814487022,1319.2657352195877,54.61549167394552,34.40685277344917,20.174948982531305,-0.3612993055297409,108.6102887120528,0.5980324103024641,38.22073719637235
+2009,Southeast,-20.08610065182105,-41.70437467853287,23.396229251011675,1441.0340009008864,64.86408524311952,30.995889136150936,22.006966142317356,0.0034251309949127062,230.37855439335158,0.8452648209795927,41.84649068078829
+2010,Southeast,-19.209193878309918,-48.78852060980094,23.348076539479127,1117.1211367527221,56.132807567714956,33.900771178042895,23.816342021489287,-0.0447275805376357,-93.53430975481274,0.3672213812903554,39.76825698694564
+2011,Southeast,-19.655354773763946,-45.57313155020834,22.979809268324786,1144.5145291566982,61.61116316663697,33.60146714379692,20.482851374035494,-0.4129948516919768,-66.14091735083662,0.4712400818315478,40.86976082200451
+2012,Southeast,-19.331673981892003,-47.372936302468204,24.100980303629374,1245.2839672657756,52.44654521430904,33.44623927447723,17.485639919051778,0.7081761836126113,34.62852075824071,0.5196348930816433,36.25818184378232
+2013,Southeast,-20.68837228223076,-45.6524589327279,24.126256299032743,1376.3290061543098,60.92726291162836,31.251484785721523,20.57356999921902,0.7334521790159805,165.67355964677495,1.0133837640801957,39.91842160013351
+2014,Southeast,-19.102184591589452,-43.50466819778212,23.708558841633746,1086.4582209199227,53.79309243498488,34.28955706756165,18.5429798973715,0.31575472161698315,-124.19722558761214,0.6298264251943527,37.3669980634739
+2015,Southeast,-18.37106305237601,-44.35845325089951,23.53433726972291,1227.0775252741726,50.54334848782783,33.83162344622434,19.402274644411364,0.14153314970614872,16.422078766637696,0.1386344621876996,36.187508822321846
+2016,Southeast,-20.97014951965141,-44.50675513789603,24.376882839454403,1377.7820589943544,59.65605720415113,29.44597136905704,19.662911417151495,0.9840787194376404,167.12661248681957,1.1577405600063795,38.59508771752301
+2017,Southeast,-20.88769565571456,-44.31836974276012,24.28234936988297,1023.954519964682,56.425613912731585,33.485349985128906,16.153912959273313,0.889545249866206,-186.7009265428528,1.1769544195818495,37.462024448413295
+2018,Southeast,-18.664215663679766,-43.59322351876011,23.42242799217706,1054.5017749675908,51.27794207999204,35.84450039867439,16.0730311474456,0.029623872160296827,-156.15367153994407,0.588078022482914,36.08643629583281
+2019,Southeast,-20.19134398669506,-46.01707208388621,24.372255324670228,1133.7262958756555,60.73415511467054,31.735225012700205,20.007611931920618,0.9794512046534649,-76.92915063187934,0.8249805359388296,39.81651312925447
+2020,Southeast,-18.596178607933442,-42.5863007182019,24.692004095940284,1117.5063761057631,50.4054335443252,30.368827936732554,17.737171303312813,1.2991999759235213,-93.14907040177172,1.0617417751649882,34.59397318974369
+2021,Southeast,-20.442535711892184,-42.674668059229035,24.116704867237786,1244.3036070362339,51.89966880463728,30.411645745098,13.392250446793646,0.7239007472210233,33.64816052869901,0.5247693655573789,33.90103637942241
+2022,Southeast,-18.544954423729287,-43.2299241246878,24.892166905267686,1180.6877111125186,52.21413245578431,31.578339620893647,19.83055763843366,1.4993627852509235,-29.967735395016234,0.9414911973288502,36.30832216011191
+2023,Southeast,-19.86851418319455,-42.886933114497474,24.02710650884533,972.5037940357872,46.90167032935693,26.538656788985563,18.544739510548478,0.6343023888285657,-238.15165247174764,1.2237056538566315,32.28568702160298
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..62c0cf2
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+ Bee Migration Study
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Starting the Python runtime…
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/scripts/charts/bar-chart.js b/docs/scripts/charts/bar-chart.js
new file mode 100644
index 0000000..fed1f2d
--- /dev/null
+++ b/docs/scripts/charts/bar-chart.js
@@ -0,0 +1,108 @@
+import { svg, text } from "./dom.js";
+import { linearScale } from "./scale.js";
+import { hideTooltip, showTooltip } from "./tooltip.js";
+
+const ROW_HEIGHT = 30;
+const THICKNESS = 14;
+const END_RADIUS = 4;
+const LABEL_SHARE = 0.38;
+const WIDEST_LABEL = 190;
+const VALUE_ROOM = 52;
+const PADDING = 6;
+const LABEL_SHARE_WHEN_NARROW = 0.46;
+const NARROW = 420;
+const CHARACTER_WIDTH = 6.2;
+const ELLIPSIS = "…";
+
+/** Shorten a label that cannot fit its column; the full text stays in the tooltip. */
+function fitted(label, room) {
+ const capacity = Math.floor(room / CHARACTER_WIDTH);
+ if (label.length <= capacity) {
+ return label;
+ }
+ return `${label.slice(0, Math.max(capacity - 1, 1))}${ELLIPSIS}`;
+}
+
+/** A bar from the baseline to `end`, rounded only at the data end. */
+function barPath(baseline, end, top) {
+ const direction = Math.sign(end - baseline) || 1;
+ const radius = Math.min(END_RADIUS, Math.abs(end - baseline));
+ const corner = end - direction * radius;
+ const sweep = direction > 0 ? 1 : 0;
+ return [
+ `M${baseline},${top}`,
+ `H${corner}`,
+ `A${radius},${radius} 0 0 ${sweep} ${end},${top + radius}`,
+ `V${top + THICKNESS - radius}`,
+ `A${radius},${radius} 0 0 ${sweep} ${corner},${top + THICKNESS}`,
+ `H${baseline}Z`,
+ ].join("");
+}
+
+function rowOf(record, index, layout, title) {
+ const top = PADDING + index * ROW_HEIGHT;
+ const middle = top + ROW_HEIGHT / 2;
+ const end = layout.x(record.value);
+ const emphasis = record.emphasis ? " is-emphasis" : "";
+ const strong = record.emphasis ? " is-strong" : "";
+ const group = svg("g", { class: "bar-row" }, [
+ text(fitted(record.label, layout.labelEnd), {
+ class: `mark-label${strong}`,
+ x: layout.labelEnd,
+ y: middle,
+ "text-anchor": "end",
+ "dominant-baseline": "middle",
+ }),
+ svg("path", {
+ class: `bar${emphasis}`,
+ d: barPath(layout.x(0), end, middle - THICKNESS / 2),
+ }),
+ text(record.text, {
+ class: `mark-label${strong}`,
+ x: Math.max(end, layout.x(0)) + 6,
+ y: middle,
+ "dominant-baseline": "middle",
+ }),
+ svg("rect", { class: "hit-area", x: 0, y: top, width: layout.width, height: ROW_HEIGHT }),
+ ]);
+ group.addEventListener("pointermove", (event) =>
+ showTooltip({ x: event.clientX, y: event.clientY }, title, [
+ { name: record.label, value: record.text },
+ ]),
+ );
+ group.addEventListener("pointerleave", hideTooltip);
+ return group;
+}
+
+/**
+ * Horizontal bars sharing a zero baseline, each labelled with its value.
+ * @param {{ width: number, label: string, title: string,
+ * rows: { label: string, value: number, text: string, emphasis?: boolean }[] }} spec
+ */
+export function barChart(spec) {
+ const values = spec.rows.map((row) => row.value);
+ const share = spec.width < NARROW ? LABEL_SHARE_WHEN_NARROW : LABEL_SHARE;
+ const labelEnd = Math.min(spec.width * share, WIDEST_LABEL);
+ const layout = {
+ width: spec.width,
+ labelEnd,
+ x: linearScale(
+ [Math.min(0, ...values), Math.max(0, ...values)],
+ [labelEnd + 10, spec.width - VALUE_ROOM],
+ ),
+ };
+ const height = spec.rows.length * ROW_HEIGHT + PADDING * 2;
+ const baseline = svg("line", {
+ class: "axis-line",
+ x1: layout.x(0),
+ x2: layout.x(0),
+ y1: PADDING,
+ y2: height - PADDING,
+ });
+ const rows = spec.rows.map((row, index) => rowOf(row, index, layout, spec.title));
+ return svg(
+ "svg",
+ { viewBox: `0 0 ${spec.width} ${height}`, role: "img", "aria-label": spec.label },
+ [...rows, baseline],
+ );
+}
diff --git a/docs/scripts/charts/dom.js b/docs/scripts/charts/dom.js
new file mode 100644
index 0000000..3adde95
--- /dev/null
+++ b/docs/scripts/charts/dom.js
@@ -0,0 +1,22 @@
+const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
+
+/** Create an SVG element. Attributes are set as given; no markup is parsed. */
+export function svg(tag, attributes = {}, children = []) {
+ const node = document.createElementNS(SVG_NAMESPACE, tag);
+ Object.entries(attributes).forEach(([name, value]) => node.setAttribute(name, value));
+ node.append(...children);
+ return node;
+}
+
+/** Create an HTML element from DOM properties such as `className` and `textContent`. */
+export function html(tag, properties = {}, children = []) {
+ const node = Object.assign(document.createElement(tag), properties);
+ node.append(...children);
+ return node;
+}
+
+export function text(content, attributes = {}) {
+ const node = svg("text", attributes);
+ node.textContent = content;
+ return node;
+}
diff --git a/docs/scripts/charts/heatmap.js b/docs/scripts/charts/heatmap.js
new file mode 100644
index 0000000..f51468e
--- /dev/null
+++ b/docs/scripts/charts/heatmap.js
@@ -0,0 +1,80 @@
+import { svg, text } from "./dom.js";
+import { hideTooltip, showTooltip } from "./tooltip.js";
+
+const LABEL_SHARE = 0.44;
+const WIDEST_LABEL = 190;
+const LARGEST_CELL = 44;
+const HEADER = 22;
+
+function fillOf(value) {
+ const pole = value < 0 ? "--diverging-negative" : "--diverging-positive";
+ const share = Math.round(Math.abs(value) * 100);
+ return `color-mix(in oklab, var(${pole}) ${share}%, var(--diverging-neutral))`;
+}
+
+function cellOf(spec, layout, row, column) {
+ const value = spec.matrix[row][column];
+ const cell = svg("rect", {
+ class: "heat-cell",
+ x: layout.left + column * layout.cell,
+ y: HEADER + row * layout.cell,
+ width: layout.cell,
+ height: layout.cell,
+ });
+ cell.style.fill = fillOf(value);
+ cell.addEventListener("pointermove", (event) =>
+ showTooltip({ x: event.clientX, y: event.clientY }, spec.labels[row], [
+ { name: spec.labels[column], value: spec.format(value) },
+ ]),
+ );
+ cell.addEventListener("pointerleave", hideTooltip);
+ return cell;
+}
+
+function rowLabel(spec, layout, row) {
+ return text(`${row + 1} ${spec.labels[row]}`, {
+ class: "mark-label",
+ x: layout.left - 8,
+ y: HEADER + (row + 0.5) * layout.cell,
+ "text-anchor": "end",
+ "dominant-baseline": "middle",
+ });
+}
+
+function columnLabel(layout, column) {
+ return text(String(column + 1), {
+ class: "tick-label",
+ x: layout.left + (column + 0.5) * layout.cell,
+ y: HEADER - 8,
+ "text-anchor": "middle",
+ });
+}
+
+/**
+ * A square matrix of values between -1 and 1 on a diverging scale.
+ * Columns are numbered to match the numbered row labels.
+ * @param {{ width: number, label: string, labels: string[], matrix: number[][],
+ * format: (value: number) => string }} spec
+ */
+export function heatmap(spec) {
+ const size = spec.labels.length;
+ const left = Math.min(spec.width * LABEL_SHARE, WIDEST_LABEL);
+ const layout = { left, cell: Math.min((spec.width - left) / size, LARGEST_CELL) };
+ const indexes = Array.from({ length: size }, (_, index) => index);
+ const cells = indexes.flatMap((row) =>
+ indexes.map((column) => cellOf(spec, layout, row, column)),
+ );
+ return svg(
+ "svg",
+ {
+ viewBox: `0 0 ${spec.width} ${HEADER + size * layout.cell}`,
+ role: "img",
+ "aria-label": spec.label,
+ },
+ [
+ ...indexes.map((row) => rowLabel(spec, layout, row)),
+ ...indexes.map((column) => columnLabel(layout, column)),
+ ...cells,
+ ],
+ );
+}
diff --git a/docs/scripts/charts/line-chart.js b/docs/scripts/charts/line-chart.js
new file mode 100644
index 0000000..1b002f8
--- /dev/null
+++ b/docs/scripts/charts/line-chart.js
@@ -0,0 +1,189 @@
+import { svg, text } from "./dom.js";
+import { extent, linearScale, niceTicks } from "./scale.js";
+import { hideTooltip, showTooltip } from "./tooltip.js";
+
+const MARGIN = { top: 12, right: 18, bottom: 28, left: 46 };
+const HEIGHT_RATIO = 0.5;
+const HEIGHT_LIMITS = [220, 340];
+const RING_RADIUS = 4.5;
+const DOT_RADIUS = 4;
+const WIDE_ENOUGH_FOR_MORE_TICKS = 480;
+
+function frameFor(width) {
+ const [shortest, tallest] = HEIGHT_LIMITS;
+ const height = Math.min(Math.max(width * HEIGHT_RATIO, shortest), tallest);
+ return {
+ width,
+ height,
+ left: MARGIN.left,
+ right: width - MARGIN.right,
+ top: MARGIN.top,
+ bottom: height - MARGIN.bottom,
+ };
+}
+
+function scalesFor(series, frame) {
+ const points = series.flatMap((entry) => entry.points);
+ const positions = [...new Set(points.map((point) => point.x))].sort((a, b) => a - b);
+ const ticks = niceTicks(extent(points.map((point) => point.y)));
+ return {
+ positions,
+ ticks,
+ x: linearScale(extent(positions), [frame.left, frame.right]),
+ y: linearScale([ticks[0], ticks.at(-1)], [frame.bottom, frame.top]),
+ };
+}
+
+function valueAxis({ ticks, y }, frame, format) {
+ return ticks.flatMap((tick) => [
+ svg("line", { class: "grid-line", x1: frame.left, x2: frame.right, y1: y(tick), y2: y(tick) }),
+ text(format(tick), {
+ class: "tick-label",
+ x: frame.left - 8,
+ y: y(tick),
+ "text-anchor": "end",
+ "dominant-baseline": "middle",
+ }),
+ ]);
+}
+
+function positionAxis({ positions, x }, frame) {
+ const count = frame.width > WIDE_ENOUGH_FOR_MORE_TICKS ? 6 : 3;
+ const [first, last] = extent(positions);
+ const shown = niceTicks([first, last], count).filter((tick) => tick >= first && tick <= last);
+ const labels = shown.map((tick) =>
+ text(String(tick), { class: "tick-label", x: x(tick), y: frame.bottom + 18, "text-anchor": "middle" }),
+ );
+ const baseline = svg("line", {
+ class: "axis-line",
+ x1: frame.left,
+ x2: frame.right,
+ y1: frame.bottom,
+ y2: frame.bottom,
+ });
+ return [baseline, ...labels];
+}
+
+function pathOf(points, { x, y }) {
+ return points.map((point, index) => `${index === 0 ? "M" : "L"}${x(point.x)},${y(point.y)}`).join("");
+}
+
+function markOf(entry, scales) {
+ if (!entry.ring) {
+ return svg("path", { class: `series-line ${entry.slot}`, d: pathOf(entry.points, scales) });
+ }
+ const rings = entry.points.map((point) =>
+ svg("circle", {
+ class: "series-ring",
+ cx: scales.x(point.x),
+ cy: scales.y(point.y),
+ r: RING_RADIUS,
+ }),
+ );
+ return svg("g", { class: entry.slot }, rings);
+}
+
+function readingsAt(series, position) {
+ return series
+ .map((entry) => ({ entry, point: entry.points.find((point) => point.x === position) }))
+ .filter(({ point }) => point !== undefined);
+}
+
+function nearestIndex(positions, x, pixel) {
+ const distances = positions.map((position) => Math.abs(x(position) - pixel));
+ return distances.indexOf(Math.min(...distances));
+}
+
+/** A crosshair that follows the pointer or the arrow keys and reports every series. */
+function attachCrosshair(root, spec, scales, frame) {
+ const rule = svg("line", { class: "crosshair", y1: frame.top, y2: frame.bottom });
+ const dots = svg("g");
+ const layer = svg("g", { class: "is-hidden" }, [rule, dots]);
+ const hit = svg("rect", {
+ class: "hit-area",
+ x: frame.left,
+ y: frame.top,
+ width: frame.right - frame.left,
+ height: frame.bottom - frame.top,
+ });
+ let current = scales.positions.length - 1;
+
+ const reveal = (index, viewportPoint) => {
+ current = index;
+ const position = scales.positions[index];
+ const readings = readingsAt(spec.series, position);
+ rule.setAttribute("x1", scales.x(position));
+ rule.setAttribute("x2", scales.x(position));
+ dots.replaceChildren(
+ ...readings.map(({ entry, point }) =>
+ svg("circle", {
+ class: `series-dot ${entry.slot}`,
+ cx: scales.x(position),
+ cy: scales.y(point.y),
+ r: DOT_RADIUS,
+ }),
+ ),
+ );
+ layer.classList.remove("is-hidden");
+ const rows = readings.map(({ entry, point }) => ({
+ slot: entry.slot,
+ name: entry.name,
+ value: spec.format(point.y),
+ }));
+ showTooltip(viewportPoint, String(position), rows);
+ };
+
+ const conceal = () => {
+ layer.classList.add("is-hidden");
+ hideTooltip();
+ };
+
+ const anchorOf = (index) => {
+ const box = root.getBoundingClientRect();
+ const scale = box.width / frame.width;
+ return { x: box.left + scales.x(scales.positions[index]) * scale, y: box.top + frame.top * scale };
+ };
+
+ hit.addEventListener("pointermove", (event) => {
+ const box = root.getBoundingClientRect();
+ const pixel = (event.clientX - box.left) * (frame.width / box.width);
+ reveal(nearestIndex(scales.positions, scales.x, pixel), { x: event.clientX, y: event.clientY });
+ });
+ hit.addEventListener("pointerleave", conceal);
+ root.addEventListener("focus", () => reveal(current, anchorOf(current)));
+ root.addEventListener("blur", conceal);
+ root.addEventListener("keydown", (event) => {
+ const step = { ArrowLeft: -1, ArrowRight: 1 }[event.key];
+ if (step === undefined) {
+ return;
+ }
+ event.preventDefault();
+ const next = Math.min(Math.max(current + step, 0), scales.positions.length - 1);
+ reveal(next, anchorOf(next));
+ });
+
+ return [layer, hit];
+}
+
+/**
+ * Lines (or rings, for `ring` series) over a shared numeric axis.
+ * @param {{ width: number, label: string, format: (value: number) => string,
+ * series: { name: string, slot: string, ring?: boolean, points: { x: number, y: number }[] }[] }} spec
+ */
+export function lineChart(spec) {
+ const frame = frameFor(spec.width);
+ const scales = scalesFor(spec.series, frame);
+ const root = svg("svg", {
+ viewBox: `0 0 ${frame.width} ${frame.height}`,
+ role: "img",
+ tabindex: "0",
+ "aria-label": `${spec.label}. Use the arrow keys to read values.`,
+ });
+ root.append(
+ ...valueAxis(scales, frame, spec.format),
+ ...positionAxis(scales, frame),
+ ...spec.series.map((entry) => markOf(entry, scales)),
+ ...attachCrosshair(root, spec, scales, frame),
+ );
+ return root;
+}
diff --git a/docs/scripts/charts/mount.js b/docs/scripts/charts/mount.js
new file mode 100644
index 0000000..6578560
--- /dev/null
+++ b/docs/scripts/charts/mount.js
@@ -0,0 +1,22 @@
+const NARROWEST = 260;
+const observers = [];
+
+/** Draw a chart into `host` at the host's width, and again whenever it changes. */
+export function mountChart(host, draw) {
+ let drawnWidth = 0;
+ const observer = new ResizeObserver(() => {
+ const width = Math.max(Math.round(host.clientWidth), NARROWEST);
+ if (width === drawnWidth) {
+ return;
+ }
+ drawnWidth = width;
+ host.replaceChildren(draw(width));
+ });
+ observer.observe(host);
+ observers.push(observer);
+}
+
+/** Stop redrawing every mounted chart, before their hosts are discarded. */
+export function unmountCharts() {
+ observers.splice(0).forEach((observer) => observer.disconnect());
+}
diff --git a/docs/scripts/charts/parts.js b/docs/scripts/charts/parts.js
new file mode 100644
index 0000000..1548eb5
--- /dev/null
+++ b/docs/scripts/charts/parts.js
@@ -0,0 +1,47 @@
+import { html } from "./dom.js";
+
+/** A legend naming every series next to a sample of its mark. */
+export function legend(items) {
+ const entries = items.map(({ name, slot, ring }) =>
+ html("li", {}, [
+ html("span", { className: `swatch ${slot}${ring ? " is-ring" : ""}` }),
+ html("span", { textContent: name }),
+ ]),
+ );
+ return html("ul", { className: "legend" }, entries);
+}
+
+function headerRow(columns) {
+ const cells = columns.map(({ label }) => html("th", { scope: "col", textContent: label }));
+ return html("thead", {}, [html("tr", {}, cells)]);
+}
+
+function bodyRow(columns, record) {
+ const cells = columns.map(({ key, format = String }) =>
+ html("td", { textContent: format(record[key]) }),
+ );
+ return html("tr", {}, cells);
+}
+
+/** The values behind a chart as a collapsible table. */
+export function tableView(columns, records) {
+ const body = html("tbody", {}, records.map((record) => bodyRow(columns, record)));
+ const table = html("table", {}, [headerRow(columns), body]);
+ return html("details", { className: "table-view" }, [
+ html("summary", { textContent: "View as table" }),
+ html("div", { className: "table-scroll" }, [table]),
+ ]);
+}
+
+/** A titled card holding a chart, its legend and its table. */
+export function chartCard({ title, note, extras = [], host, table }) {
+ const caption = html("figcaption", {}, [
+ html("h3", { textContent: title }),
+ html("p", { textContent: note }),
+ ]);
+ return html("figure", { className: "chart-card" }, [caption, ...extras, host, table]);
+}
+
+export function chartHost() {
+ return html("div", { className: "chart-host" });
+}
diff --git a/docs/scripts/charts/scale.js b/docs/scripts/charts/scale.js
new file mode 100644
index 0000000..abc1ef5
--- /dev/null
+++ b/docs/scripts/charts/scale.js
@@ -0,0 +1,30 @@
+const NICE_FACTORS = [1, 2, 5, 10];
+const SIGNIFICANT_DIGITS = 12;
+
+export function extent(values) {
+ return [Math.min(...values), Math.max(...values)];
+}
+
+export function linearScale([domainStart, domainEnd], [rangeStart, rangeEnd]) {
+ const span = domainEnd - domainStart || 1;
+ return (value) => rangeStart + ((value - domainStart) / span) * (rangeEnd - rangeStart);
+}
+
+function withoutNoise(value) {
+ return Number(value.toPrecision(SIGNIFICANT_DIGITS));
+}
+
+function niceStep(span, count) {
+ const rough = span / count;
+ const magnitude = 10 ** Math.floor(Math.log10(rough));
+ const factor = NICE_FACTORS.find((candidate) => candidate >= rough / magnitude);
+ return factor * magnitude;
+}
+
+/** Round tick values that start at or below `low` and end at or above `high`. */
+export function niceTicks([low, high], count = 5) {
+ const step = niceStep(high - low || Math.abs(low) || 1, count);
+ const first = Math.floor(low / step) * step;
+ const steps = Math.max(Math.ceil(withoutNoise((high - first) / step)), 1);
+ return Array.from({ length: steps + 1 }, (_, index) => withoutNoise(first + index * step));
+}
diff --git a/docs/scripts/charts/tooltip.js b/docs/scripts/charts/tooltip.js
new file mode 100644
index 0000000..1178295
--- /dev/null
+++ b/docs/scripts/charts/tooltip.js
@@ -0,0 +1,41 @@
+import { html } from "./dom.js";
+
+const OFFSET = 14;
+const EDGE = 8;
+
+const bubble = html("div", { className: "tooltip", hidden: true });
+bubble.setAttribute("role", "presentation");
+
+function row({ slot, name, value }) {
+ const swatch = html("span", { className: `swatch ${slot ?? "series-plain"}` });
+ return html("div", { className: "tooltip-row" }, [
+ swatch,
+ html("span", { textContent: name }),
+ html("span", { textContent: value }),
+ ]);
+}
+
+function within(position, size, limit) {
+ const flipped = position + OFFSET + size > limit - EDGE;
+ const placed = flipped ? position - OFFSET - size : position + OFFSET;
+ return Math.max(placed, EDGE);
+}
+
+/** Show the shared tooltip next to a viewport point. */
+export function showTooltip({ x, y }, title, rows) {
+ if (!bubble.isConnected) {
+ document.body.append(bubble);
+ }
+ bubble.replaceChildren(
+ html("p", { className: "tooltip-title", textContent: title }),
+ ...rows.map(row),
+ );
+ bubble.hidden = false;
+ const left = within(x, bubble.offsetWidth, window.innerWidth);
+ const top = within(y, bubble.offsetHeight, window.innerHeight);
+ bubble.style.transform = `translate(${left}px, ${top}px)`;
+}
+
+export function hideTooltip() {
+ bubble.hidden = true;
+}
diff --git a/docs/scripts/field-section.js b/docs/scripts/field-section.js
new file mode 100644
index 0000000..da205b0
--- /dev/null
+++ b/docs/scripts/field-section.js
@@ -0,0 +1,174 @@
+import { html } from "./charts/dom.js";
+import { lineChart } from "./charts/line-chart.js";
+import { mountChart } from "./charts/mount.js";
+import { chartCard, chartHost, legend, tableView } from "./charts/parts.js";
+import { formatLatitude, formatMeasure, formatNumber, formatSigned } from "./format.js";
+import { chapter, tile } from "./sections.js";
+
+const TITLE = "What the field records say";
+const SUMMARY =
+ "Observed weather and real bee records, fetched live. Unlike the chapters above, nothing here is simulated.";
+const CAVEAT =
+ "Records show where people looked for bees as much as where bees live. A centroid that moves can mean more observers in new places, so this chart cannot confirm or rule out a migration on its own.";
+
+const TOO_MANY_REQUESTS = 429;
+const ATTEMPTS = 4;
+const PAUSE_MILLISECONDS = 1500;
+
+function pause(milliseconds) {
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
+}
+
+/** Fetch JSON, waiting and trying again while the service asks to slow down. */
+async function fetched(url, attempt = 1) {
+ const response = await fetch(url);
+ if (response.status === TOO_MANY_REQUESTS && attempt < ATTEMPTS) {
+ await pause(PAUSE_MILLISECONDS * attempt);
+ return fetched(url, attempt + 1);
+ }
+ if (!response.ok) {
+ throw new Error(`${new URL(url).hostname} answered HTTP ${response.status}`);
+ }
+ return response.json();
+}
+
+/** One request at a time: GBIF rejects bursts of parallel searches. */
+async function fetchedInTurn(urls) {
+ const answers = [];
+ for (const url of urls) {
+ answers.push(await fetched(url));
+ }
+ return answers;
+}
+
+/** Fetch the weather and the record counts, then let the package summarise them. */
+async function fieldReport(runtime) {
+ const requests = await runtime.call("field_requests");
+ const [climate, answers] = await Promise.all([
+ fetched(requests.climate),
+ fetchedInTurn(requests.bands.map((band) => band.url)),
+ ]);
+ const bands = requests.bands.map((band, index) => ({
+ south: band.south,
+ north: band.north,
+ answer: answers[index],
+ }));
+ return runtime.call("field_report", { climate, bands });
+}
+
+function tiles(report) {
+ const years = report.centroids.map((row) => row.year);
+ return html("div", { className: "tiles" }, [
+ tile(
+ "Observed warming",
+ `${formatSigned(report.warming_per_decade, 2)} °C`,
+ "per decade at the five region centres (ERA5)",
+ ),
+ tile(
+ "Record centroid",
+ `${formatSigned(report.latitude_shift_per_decade, 1)}°`,
+ "latitude per decade; negative is southward",
+ ),
+ tile(
+ "Bee records",
+ formatNumber(report.total_records, 0),
+ `Apidae in South America, ${Math.min(...years)}–${Math.max(...years)} (GBIF)`,
+ ),
+ ]);
+}
+
+function climateCard(report) {
+ const host = chartHost();
+ const format = (value) => formatMeasure("temperature", value);
+ const series = report.regions.map((region, index) => ({
+ name: region,
+ slot: `series-${index + 1}`,
+ points: report.climate
+ .filter((row) => row.region === region)
+ .map((row) => ({ x: row.year, y: row.temperature })),
+ }));
+ mountChart(host, (width) =>
+ lineChart({ width, series, format, label: "Observed mean temperature by region and year" }),
+ );
+ const columns = [
+ { key: "year", label: "Year" },
+ { key: "region", label: "Region" },
+ { key: "temperature", label: "Mean temperature", format },
+ ];
+ return chartCard({
+ title: "Observed temperature by region",
+ note: "Yearly mean of daily temperature at each region's centre. Source: Open-Meteo, ERA5 reanalysis.",
+ extras: [legend(series)],
+ host,
+ table: tableView(columns, report.climate),
+ });
+}
+
+function centroidCard(report) {
+ const host = chartHost();
+ const series = [
+ {
+ name: "Mean latitude of records",
+ slot: "series-plain",
+ points: report.centroids.map((row) => ({ x: row.year, y: row.mean_latitude })),
+ },
+ ];
+ mountChart(host, (width) =>
+ lineChart({
+ width,
+ series,
+ format: formatLatitude,
+ label: "Mean latitude of bee records by year",
+ }),
+ );
+ const columns = [
+ { key: "year", label: "Year" },
+ { key: "records", label: "Records", format: (value) => formatNumber(value, 0) },
+ { key: "mean_latitude", label: "Mean latitude", format: formatLatitude },
+ ];
+ return chartCard({
+ title: "Where bees were recorded",
+ note: `Mean latitude of Apidae records in South America, from counts in 5° bands. ${CAVEAT}`,
+ host,
+ table: tableView(columns, report.centroids),
+ });
+}
+
+function invitation(onLoad, message = "") {
+ const button = html("button", {
+ type: "button",
+ className: "run-button",
+ textContent: "Load field data",
+ });
+ const note = html("p", { className: "status", textContent: message });
+ note.setAttribute("role", "status");
+ note.dataset.state = message === "" ? "working" : "failed";
+ button.addEventListener("click", () => {
+ button.disabled = true;
+ note.dataset.state = "working";
+ note.textContent = "Fetching from GBIF and Open-Meteo…";
+ onLoad();
+ });
+ return html("div", { className: "field-invite" }, [
+ html("p", {
+ textContent:
+ "Loading sends about fifteen requests from your browser to api.gbif.org and archive-api.open-meteo.com.",
+ }),
+ button,
+ note,
+ ]);
+}
+
+/** Offer the field chapter in `container`, loading its data only when asked. */
+export function mountField(container, runtime) {
+ const show = (body) => container.replaceChildren(chapter("4", TITLE, SUMMARY, body));
+ const load = async () => {
+ try {
+ const report = await fieldReport(runtime);
+ show([tiles(report), climateCard(report), centroidCard(report)]);
+ } catch (error) {
+ show([invitation(load, `The field data could not be loaded: ${error.message}`)]);
+ }
+ };
+ show([invitation(load)]);
+}
diff --git a/docs/scripts/format.js b/docs/scripts/format.js
new file mode 100644
index 0000000..4bb829c
--- /dev/null
+++ b/docs/scripts/format.js
@@ -0,0 +1,48 @@
+const MISSING = "–";
+const MINUS = "−";
+const UNITS = {
+ temperature: { suffix: " °C", decimals: 1 },
+ precipitation: { suffix: " mm", decimals: 0 },
+};
+const PLAIN = { suffix: "", decimals: 1 };
+
+export function humanize(name) {
+ const words = name.replaceAll("_", " ");
+ return words.charAt(0).toUpperCase() + words.slice(1);
+}
+
+export function formatNumber(value, decimals = 1) {
+ if (value === null || value === undefined || Number.isNaN(value)) {
+ return MISSING;
+ }
+ return value.toLocaleString("en-US", {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ });
+}
+
+export function formatSigned(value, decimals = 1) {
+ const text = formatNumber(Math.abs(value), decimals);
+ if (Number(text) === 0) {
+ return text;
+ }
+ return `${value > 0 ? "+" : MINUS}${text}`;
+}
+
+export function formatLatitude(value) {
+ const text = formatNumber(value, 1);
+ if (text === MISSING) {
+ return text;
+ }
+ return `${formatNumber(Math.abs(value), 1)}° ${value < 0 ? "S" : "N"}`;
+}
+
+export function unitOf(measure) {
+ return (UNITS[measure] ?? PLAIN).suffix.trim();
+}
+
+export function formatMeasure(measure, value) {
+ const { suffix, decimals } = UNITS[measure] ?? PLAIN;
+ const text = formatNumber(value, decimals);
+ return text === MISSING ? text : `${text}${suffix}`;
+}
diff --git a/docs/scripts/main.js b/docs/scripts/main.js
new file mode 100644
index 0000000..6250bd8
--- /dev/null
+++ b/docs/scripts/main.js
@@ -0,0 +1,113 @@
+import { unmountCharts } from "./charts/mount.js";
+import { hideTooltip } from "./charts/tooltip.js";
+import { mountField } from "./field-section.js";
+import { reportOf } from "./sections.js";
+import { StudyRuntime } from "./study-client.js";
+import { readState, studyRequest, writeState } from "./url-state.js";
+
+const form = document.querySelector("#controls");
+const status = document.querySelector("#status");
+const report = document.querySelector("#report");
+const runButton = form.querySelector(".run-button");
+const fields = form.elements;
+
+let state = readState(window.location.search);
+let study = null;
+
+function announce(message, failed = false) {
+ status.textContent = message;
+ status.dataset.state = failed ? "failed" : "working";
+}
+
+function showState() {
+ fields.seed.value = state.seed;
+ fields.holdout.value = state.holdout;
+ fields.holdoutValue.value = state.holdout;
+ fields.target.value = state.target;
+ fields.thorough.checked = state.thorough;
+ fields.measure.value = state.measure;
+}
+
+function stateFromForm() {
+ const query = new URLSearchParams({
+ seed: fields.seed.value,
+ holdout: fields.holdout.value,
+ target: fields.target.value,
+ thorough: fields.thorough.checked ? "1" : "0",
+ measure: fields.measure.value,
+ region: fields.region.value,
+ });
+ return readState(`?${query}`);
+}
+
+function rememberState() {
+ const address = `${window.location.pathname}${writeState(state)}`;
+ window.history.replaceState(null, "", address);
+}
+
+function offerRegions(regions) {
+ const chosen = regions.includes(state.region) ? state.region : regions[0];
+ const options = regions.map((region) => new Option(region, region, false, region === chosen));
+ fields.region.replaceChildren(...options);
+ return chosen;
+}
+
+function draw() {
+ const region = offerRegions(study.regions);
+ unmountCharts();
+ hideTooltip();
+ report.replaceChildren(...reportOf(study, { measure: state.measure, region }));
+}
+
+function setBusy(busy) {
+ report.setAttribute("aria-busy", String(busy));
+ runButton.disabled = busy;
+}
+
+async function runStudy(runtime) {
+ setBusy(true);
+ const started = performance.now();
+ try {
+ study = await runtime.run(studyRequest(state));
+ draw();
+ const seconds = ((performance.now() - started) / 1000).toFixed(1);
+ announce(`Study computed in your browser in ${seconds} s.`);
+ } catch (error) {
+ announce(`The study could not be computed: ${error.message}`, true);
+ } finally {
+ setBusy(false);
+ }
+}
+
+function start() {
+ const runtime = new StudyRuntime((message) => announce(message));
+ showState();
+
+ form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ state = stateFromForm();
+ showState();
+ rememberState();
+ announce("Running the study…");
+ runStudy(runtime);
+ });
+
+ fields.holdout.addEventListener("input", () => {
+ fields.holdoutValue.value = fields.holdout.value;
+ });
+
+ [fields.measure, fields.region].forEach((field) =>
+ field.addEventListener("change", () => {
+ state = { ...state, measure: fields.measure.value, region: fields.region.value };
+ rememberState();
+ if (study !== null) {
+ draw();
+ }
+ }),
+ );
+
+ mountField(document.querySelector("#field"), runtime);
+ runStudy(runtime);
+}
+
+start();
diff --git a/docs/scripts/sections.js b/docs/scripts/sections.js
new file mode 100644
index 0000000..5c8a83f
--- /dev/null
+++ b/docs/scripts/sections.js
@@ -0,0 +1,260 @@
+import { barChart } from "./charts/bar-chart.js";
+import { html } from "./charts/dom.js";
+import { heatmap } from "./charts/heatmap.js";
+import { lineChart } from "./charts/line-chart.js";
+import { mountChart } from "./charts/mount.js";
+import { chartCard, chartHost, legend, tableView } from "./charts/parts.js";
+import { formatMeasure, formatNumber, formatSigned, humanize, unitOf } from "./format.js";
+
+const YEARS_PER_DECADE = 10;
+
+function mean(values) {
+ return values.reduce((total, value) => total + value, 0) / values.length;
+}
+
+function slotOf(study, region) {
+ return `series-${study.regions.indexOf(region) + 1}`;
+}
+
+export function chapter(number, title, summary, body, split = false) {
+ const heading = html("div", { className: "chapter-heading" }, [
+ html("span", { className: "chapter-number", textContent: number }),
+ html("h2", { textContent: title }),
+ html("p", { textContent: summary }),
+ ]);
+ const content = html("div", { className: `chapter-body${split ? " is-split" : ""}` }, body);
+ return html("section", { className: "chapter" }, [heading, content]);
+}
+
+export function tile(label, value, note, kind = "") {
+ return html("div", { className: "tile" }, [
+ html("span", { className: "tile-label", textContent: label }),
+ html("span", { className: `tile-value ${kind}`.trim(), textContent: value }),
+ html("span", { className: "tile-note", textContent: note }),
+ ]);
+}
+
+function decadeChange(study, measure) {
+ const change = mean(study.trends.map((trend) => trend[measure])) * YEARS_PER_DECADE;
+ return `${formatSigned(change, 1)} ${unitOf(measure)}`.trim();
+}
+
+function tiles(study) {
+ const target = study.request.target;
+ const best = study.models.find((model) => model.model === study.best);
+ const years = study.observations.map((record) => record.year);
+ return html("div", { className: "tiles" }, [
+ tile("Warming", decadeChange(study, "temperature"), "per decade, mean of all regions"),
+ tile(humanize(target), decadeChange(study, target), "per decade, mean of all regions"),
+ tile(
+ "Best model",
+ study.best,
+ `R² ${formatNumber(best.test_r2, 2)} on ${study.request.holdout_years} held-out years`,
+ "is-name",
+ ),
+ tile(
+ "Records",
+ formatNumber(study.observations.length, 0),
+ `${study.regions.length} regions · ${Math.min(...years)}–${Math.max(...years)}`,
+ ),
+ ]);
+}
+
+function seriesByRegion(study, measure) {
+ return study.regions.map((region) => ({
+ name: region,
+ slot: slotOf(study, region),
+ points: study.observations
+ .filter((record) => record.region === region)
+ .map((record) => ({ x: record.year, y: record[measure] })),
+ }));
+}
+
+function yearsByRegion(study, measure) {
+ const years = [...new Set(study.observations.map((record) => record.year))];
+ return years.map((year) => {
+ const rows = study.observations.filter((record) => record.year === year);
+ const cells = rows.map((record) => [record.region, record[measure]]);
+ return { year, ...Object.fromEntries(cells) };
+ });
+}
+
+function trendCard(study, measure) {
+ const host = chartHost();
+ const series = seriesByRegion(study, measure);
+ const format = (value) => formatMeasure(measure, value);
+ mountChart(host, (width) =>
+ lineChart({ width, series, format, label: `${humanize(measure)} by region and year` }),
+ );
+ const columns = [
+ { key: "year", label: "Year" },
+ ...study.regions.map((region) => ({ key: region, label: region, format })),
+ ];
+ return chartCard({
+ title: `${humanize(measure)} by region`,
+ note: "Yearly values. Hover, or focus the chart and use the arrow keys.",
+ extras: [legend(series)],
+ host,
+ table: tableView(columns, yearsByRegion(study, measure)),
+ });
+}
+
+function correlationCard(study) {
+ const host = chartHost();
+ const labels = study.correlations.variables.map(humanize);
+ const format = (value) => formatSigned(value, 2);
+ mountChart(host, (width) =>
+ heatmap({
+ width,
+ labels,
+ matrix: study.correlations.matrix,
+ format,
+ label: "Correlation between every pair of variables",
+ }),
+ );
+ const scale = html("div", { className: "scale-legend" }, [
+ html("span", { textContent: "−1" }),
+ html("span", { className: "scale-ramp" }),
+ html("span", { textContent: "+1" }),
+ ]);
+ const records = study.correlations.matrix.map((row, index) => ({
+ variable: labels[index],
+ ...Object.fromEntries(row.map((value, column) => [column, value])),
+ }));
+ const columns = [
+ { key: "variable", label: "Variable" },
+ ...labels.map((_, column) => ({ key: column, label: String(column + 1), format })),
+ ];
+ return chartCard({
+ title: "Correlation matrix",
+ note: "Pearson correlation across all regions and years. Columns follow the row numbers.",
+ extras: [scale],
+ host,
+ table: tableView(columns, records),
+ });
+}
+
+function pairList(study) {
+ const items = study.pairs.map((pair) =>
+ html("li", {}, [
+ html("span", { textContent: `${humanize(pair.first)} × ${humanize(pair.second)}` }),
+ html("strong", { textContent: formatSigned(pair.coefficient, 2) }),
+ ]),
+ );
+ return html("figure", { className: "chart-card" }, [
+ html("figcaption", {}, [
+ html("h3", { textContent: "Strongest pairs" }),
+ html("p", { textContent: "Largest correlations between two different variables." }),
+ ]),
+ html("ul", { className: "pair-list" }, items),
+ ]);
+}
+
+function modelCard(study) {
+ const host = chartHost();
+ const rows = study.models.map((model) => ({
+ label: model.model,
+ value: model.test_r2,
+ text: formatNumber(model.test_r2, 2),
+ emphasis: model.model === study.best,
+ }));
+ mountChart(host, (width) =>
+ barChart({ width, rows, title: "R² on held-out years", label: "Model accuracy on held-out years" }),
+ );
+ const decimals = (value) => formatNumber(value, 2);
+ const columns = [
+ { key: "model", label: "Model" },
+ { key: "test_r2", label: "Test R²", format: decimals },
+ { key: "test_rmse", label: "Test RMSE", format: decimals },
+ { key: "test_mae", label: "Test MAE", format: decimals },
+ { key: "train_r2", label: "Train R²", format: decimals },
+ ];
+ return chartCard({
+ title: "Model accuracy",
+ note: `R² on the ${study.request.holdout_years} most recent years, which no model saw while training.`,
+ host,
+ table: tableView(columns, study.models),
+ });
+}
+
+function forecastSeries(study, region) {
+ const target = study.request.target;
+ const observed = study.observations
+ .filter((record) => record.region === region)
+ .map((record) => ({ x: record.year, y: record[target] }));
+ const predicted = study.forecasts
+ .filter((row) => row.region === region)
+ .map((row) => ({ x: row.year, y: row[study.best] }));
+ return [
+ { name: "Observed", slot: slotOf(study, region), points: observed },
+ { name: `Predicted (${study.best})`, slot: "series-plain", ring: true, points: predicted },
+ ];
+}
+
+function forecastCard(study, region) {
+ const host = chartHost();
+ const target = study.request.target;
+ const series = forecastSeries(study, region);
+ const format = (value) => formatMeasure(target, value);
+ mountChart(host, (width) =>
+ lineChart({ width, series, format, label: `Observed and predicted ${humanize(target)} in ${region}` }),
+ );
+ const columns = [
+ { key: "year", label: "Year" },
+ { key: "actual", label: "Observed", format },
+ ...study.models.map((model) => ({ key: model.model, label: model.model, format })),
+ ];
+ return chartCard({
+ title: `${humanize(target)} in ${region}`,
+ note: "The line is what was observed; rings are the best model's forecasts for the held-out years.",
+ extras: [legend(series)],
+ host,
+ table: tableView(columns, study.forecasts.filter((row) => row.region === region)),
+ });
+}
+
+function importanceCard(study) {
+ const host = chartHost();
+ const rows = study.importance.map((entry) => ({
+ label: humanize(entry.feature),
+ value: entry.importance,
+ text: formatNumber(entry.importance, 2),
+ }));
+ mountChart(host, (width) =>
+ barChart({ width, rows, title: "Permutation importance", label: "Most influential features" }),
+ );
+ const columns = [
+ { key: "feature", label: "Feature", format: humanize },
+ { key: "importance", label: "Importance", format: (value) => formatNumber(value, 3) },
+ ];
+ return chartCard({
+ title: `What ${study.best} relies on`,
+ note: "Drop in R² when a feature is shuffled, measured on the training years.",
+ host,
+ table: tableView(columns, study.importance),
+ });
+}
+
+/** Every chapter of the report for one study and the visitor's view choices. */
+export function reportOf(study, view) {
+ const target = humanize(study.request.target).toLowerCase();
+ return [
+ chapter("1", "The climate signal", "How each region changed over the study period.", [
+ tiles(study),
+ trendCard(study, view.measure),
+ ]),
+ chapter(
+ "2",
+ "What moves together",
+ "Variables that rise and fall together across regions and years. Correlation alone does not show cause.",
+ [correlationCard(study), pairList(study)],
+ true,
+ ),
+ chapter(
+ "3",
+ "Can it be forecast?",
+ `Four models learn ${target} from climate and from last year's value, then predict years they never saw.`,
+ [modelCard(study), forecastCard(study, view.region), importanceCard(study)],
+ ),
+ ];
+}
diff --git a/docs/scripts/study-client.js b/docs/scripts/study-client.js
new file mode 100644
index 0000000..192dd62
--- /dev/null
+++ b/docs/scripts/study-client.js
@@ -0,0 +1,63 @@
+/** Runs the Python package in a background worker so the page stays responsive. */
+export class StudyRuntime {
+ #worker;
+ #pending = new Map();
+ #lastId = 0;
+
+ /** @param {(message: string) => void} onStatus called with progress messages */
+ constructor(onStatus) {
+ this.#worker = new Worker(new URL("./study-worker.js", import.meta.url), { type: "module" });
+ this.#worker.addEventListener("message", ({ data }) => this.#receive(data, onStatus));
+ this.#worker.addEventListener("error", () => this.#failAll("The Python runtime could not start."));
+ }
+
+ /** @returns {Promise} the study computed for `request` */
+ run(request) {
+ return this.call("run_study", request);
+ }
+
+ /**
+ * Call one of the package's JSON functions.
+ * @param {"run_study" | "field_requests" | "field_report"} name
+ * @param {object} [argument] sent to Python as JSON
+ * @returns {Promise}
+ */
+ call(name, argument) {
+ this.#lastId += 1;
+ const id = this.#lastId;
+ return new Promise((resolve, reject) => {
+ this.#pending.set(id, { resolve, reject });
+ this.#worker.postMessage({ id, call: name, argument });
+ });
+ }
+
+ #receive(data, onStatus) {
+ if (data.kind === "status") {
+ onStatus(data.message);
+ return;
+ }
+ if (data.kind === "failed" && data.id === undefined) {
+ this.#failAll(data.message);
+ return;
+ }
+ this.#settle(data);
+ }
+
+ #settle(data) {
+ const waiting = this.#pending.get(data.id);
+ if (waiting === undefined) {
+ return;
+ }
+ this.#pending.delete(data.id);
+ if (data.kind === "failed") {
+ waiting.reject(new Error(data.message));
+ return;
+ }
+ waiting.resolve(data.value);
+ }
+
+ #failAll(message) {
+ this.#pending.forEach(({ reject }) => reject(new Error(message)));
+ this.#pending.clear();
+ }
+}
diff --git a/docs/scripts/study-worker.js b/docs/scripts/study-worker.js
new file mode 100644
index 0000000..e88d999
--- /dev/null
+++ b/docs/scripts/study-worker.js
@@ -0,0 +1,63 @@
+import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v314.0.7/full/pyodide.mjs";
+
+const WHEEL = "../package/bee_migration_analysis-1.0.0-py3-none-any.whl";
+const PACKAGES = ["numpy", "pandas", "scikit-learn"];
+
+function report(message) {
+ self.postMessage({ kind: "status", message });
+}
+
+async function wheelBytes() {
+ const response = await fetch(new URL(WHEEL, import.meta.url));
+ if (!response.ok) {
+ throw new Error(`Could not download the analysis package (HTTP ${response.status})`);
+ }
+ return new Uint8Array(await response.arrayBuffer());
+}
+
+/** Start Python and load the analysis package; resolves to its JSON functions. */
+async function startPython() {
+ report("Starting the Python runtime…");
+ const pyodide = await loadPyodide();
+ report("Loading numpy, pandas and scikit-learn…");
+ await pyodide.loadPackage(PACKAGES, { messageCallback: () => {} });
+ report("Loading the analysis package…");
+ pyodide.unpackArchive(await wheelBytes(), "wheel");
+ const study = pyodide.pyimport("bee_migration.web.study");
+ const field = pyodide.pyimport("bee_migration.web.field");
+ return {
+ run_study: study.run_study,
+ field_requests: field.field_requests,
+ field_report: field.field_report,
+ };
+}
+
+/** Call a Python function that takes and returns JSON text. */
+function answerOf(python, argument) {
+ if (argument === undefined) {
+ return JSON.parse(python());
+ }
+ return JSON.parse(python(JSON.stringify(argument)));
+}
+
+/** The last line of a Python traceback is the message meant for people. */
+function readable(error) {
+ const lines = String(error?.message ?? error).trim().split("\n");
+ return lines.at(-1);
+}
+
+const functions = startPython();
+
+functions.then(
+ () => self.postMessage({ kind: "ready" }),
+ (error) => self.postMessage({ kind: "failed", message: readable(error) }),
+);
+
+self.addEventListener("message", async ({ data }) => {
+ try {
+ const python = (await functions)[data.call];
+ self.postMessage({ kind: "answer", id: data.id, value: answerOf(python, data.argument) });
+ } catch (error) {
+ self.postMessage({ kind: "failed", id: data.id, message: readable(error) });
+ }
+});
diff --git a/docs/scripts/url-state.js b/docs/scripts/url-state.js
new file mode 100644
index 0000000..c8b307a
--- /dev/null
+++ b/docs/scripts/url-state.js
@@ -0,0 +1,57 @@
+export const TARGETS = ["bee_abundance", "species_diversity", "honey_production"];
+export const MEASURES = ["temperature", "precipitation", ...TARGETS];
+export const LIMITS = { seed: [0, 9999], holdout: [2, 8] };
+export const DEFAULTS = Object.freeze({
+ seed: 42,
+ holdout: 4,
+ target: "bee_abundance",
+ thorough: false,
+ measure: "temperature",
+ region: "",
+});
+
+function boundedInteger(text, name) {
+ const [lowest, highest] = LIMITS[name];
+ const value = Number.parseInt(text, 10);
+ if (Number.isNaN(value)) {
+ return DEFAULTS[name];
+ }
+ return Math.min(Math.max(value, lowest), highest);
+}
+
+function oneOf(text, allowed, fallback) {
+ return allowed.includes(text) ? text : fallback;
+}
+
+/** Read the study and view settings from a location search string. */
+export function readState(search) {
+ const query = new URLSearchParams(search);
+ return {
+ seed: boundedInteger(query.get("seed"), "seed"),
+ holdout: boundedInteger(query.get("holdout"), "holdout"),
+ target: oneOf(query.get("target"), TARGETS, DEFAULTS.target),
+ thorough: query.get("thorough") === "1",
+ measure: oneOf(query.get("measure"), MEASURES, DEFAULTS.measure),
+ region: query.get("region") ?? DEFAULTS.region,
+ };
+}
+
+function serialized(value) {
+ return value === true ? "1" : String(value);
+}
+
+/** Write the settings that differ from the defaults as a search string. */
+export function writeState(state) {
+ const changed = Object.keys(DEFAULTS).filter((name) => state[name] !== DEFAULTS[name]);
+ const query = new URLSearchParams(changed.map((name) => [name, serialized(state[name])]));
+ return changed.length === 0 ? "" : `?${query}`;
+}
+
+export function studyRequest(state) {
+ return {
+ seed: state.seed,
+ holdout_years: state.holdout,
+ target: state.target,
+ thorough: state.thorough,
+ };
+}
diff --git a/docs/styles/charts.css b/docs/styles/charts.css
new file mode 100644
index 0000000..3da6a0d
--- /dev/null
+++ b/docs/styles/charts.css
@@ -0,0 +1,323 @@
+/* Stat tiles */
+.tiles {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
+ gap: 1px;
+ overflow: hidden;
+ border-radius: var(--radius-card);
+ background: var(--color-rule);
+ box-shadow: var(--shadow-card);
+}
+
+.tile {
+ display: grid;
+ align-content: start;
+ gap: 0.2rem;
+ min-width: 0;
+ padding: var(--space-card);
+ background: var(--color-surface);
+}
+
+.tile-label {
+ color: var(--color-ink-muted);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.tile-value {
+ font-size: var(--text-figure);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ line-height: 1.1;
+}
+
+.tile-value.is-name {
+ font-size: 1.375rem;
+ letter-spacing: -0.01em;
+ line-height: 1.25;
+ overflow-wrap: anywhere;
+}
+
+.tile-note {
+ color: var(--color-ink-soft);
+ font-size: var(--text-small);
+}
+
+/* Chart card */
+.chart-card {
+ min-width: 0;
+ margin: 0;
+ padding: var(--space-card);
+ border-radius: var(--radius-card);
+ background: var(--color-surface);
+ box-shadow: var(--shadow-card);
+}
+
+.chart-card figcaption {
+ margin-bottom: 0.75rem;
+}
+
+.chart-card h3 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.chart-card figcaption p {
+ margin: 0.15rem 0 0;
+ color: var(--color-ink-soft);
+ font-size: var(--text-small);
+}
+
+.chart-host {
+ min-width: 0;
+}
+
+.chart-host svg {
+ display: block;
+ width: 100%;
+ height: auto;
+ overflow: visible;
+}
+
+.chart-host svg:focus-visible {
+ outline-offset: 6px;
+}
+
+/* Series identity */
+.series-1 { --series: var(--series-1); }
+.series-2 { --series: var(--series-2); }
+.series-3 { --series: var(--series-3); }
+.series-4 { --series: var(--series-4); }
+.series-5 { --series: var(--series-5); }
+.series-plain { --series: var(--chart-emphasis); }
+
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 1rem;
+ margin: 0 0 0.75rem;
+ padding: 0;
+ list-style: none;
+ color: var(--color-ink-soft);
+ font-size: var(--text-small);
+}
+
+.legend li {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.swatch {
+ width: 0.875rem;
+ height: 0.1875rem;
+ border-radius: 2px;
+ background: var(--series);
+}
+
+.swatch.is-ring {
+ width: 0.625rem;
+ height: 0.625rem;
+ border: 2px solid var(--series);
+ border-radius: 50%;
+ background: var(--color-surface);
+}
+
+/* Marks */
+.grid-line {
+ stroke: var(--chart-grid);
+ stroke-width: 1;
+}
+
+.axis-line {
+ stroke: var(--chart-axis);
+ stroke-width: 1;
+}
+
+.tick-label,
+.mark-label {
+ fill: var(--color-ink-muted);
+ font-family: var(--font-text);
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+}
+
+.mark-label {
+ fill: var(--color-ink-soft);
+}
+
+.mark-label.is-strong {
+ fill: var(--color-ink);
+ font-weight: 700;
+}
+
+.series-line {
+ fill: none;
+ stroke: var(--series);
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.series-ring {
+ fill: var(--color-surface);
+ stroke: var(--series);
+ stroke-width: 2;
+}
+
+.series-dot {
+ fill: var(--series);
+ stroke: var(--color-surface);
+ stroke-width: 2;
+}
+
+.crosshair {
+ stroke: var(--chart-axis);
+ stroke-width: 1;
+}
+
+.bar {
+ fill: var(--chart-neutral);
+ transition: opacity var(--duration-fast) var(--ease-out);
+}
+
+.bar.is-emphasis {
+ fill: var(--chart-emphasis);
+}
+
+.bar-row:hover .bar,
+.heat-cell:hover {
+ opacity: 0.8;
+}
+
+.heat-cell {
+ stroke: var(--color-surface);
+ stroke-width: 2;
+}
+
+.hit-area {
+ fill: transparent;
+}
+
+.is-hidden {
+ visibility: hidden;
+}
+
+.scale-legend {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0 0 0.75rem;
+ color: var(--color-ink-soft);
+ font-size: var(--text-small);
+}
+
+.scale-ramp {
+ width: 8rem;
+ height: 0.5rem;
+ border-radius: 2px;
+ background: linear-gradient(
+ to right,
+ var(--diverging-negative),
+ var(--diverging-neutral),
+ var(--diverging-positive)
+ );
+}
+
+/* Tooltip */
+.tooltip {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 10;
+ min-width: 9rem;
+ padding: 0.5rem 0.65rem;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-control);
+ background: var(--color-surface);
+ box-shadow: var(--shadow-card);
+ color: var(--color-ink);
+ font-size: var(--text-small);
+ pointer-events: none;
+}
+
+.tooltip[hidden] {
+ display: none;
+}
+
+.tooltip-title {
+ margin: 0 0 0.25rem;
+ color: var(--color-ink-muted);
+ font-weight: 700;
+}
+
+.tooltip-row {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.tooltip-row span:last-child {
+ margin-left: auto;
+ padding-left: 0.75rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+}
+
+/* Table view */
+.table-view {
+ margin-top: 0.75rem;
+ font-size: var(--text-small);
+}
+
+.table-view summary {
+ width: fit-content;
+ color: var(--color-ink-muted);
+ cursor: pointer;
+}
+
+.table-view summary:hover {
+ color: var(--color-ink);
+}
+
+.table-scroll {
+ max-height: 18rem;
+ margin-top: 0.5rem;
+ overflow: auto;
+}
+
+.table-view table {
+ width: 100%;
+ border-collapse: collapse;
+ font-variant-numeric: tabular-nums;
+}
+
+.table-view th,
+.table-view td {
+ padding: 0.3rem 0.6rem;
+ border-bottom: 1px solid var(--color-rule);
+ text-align: right;
+ white-space: nowrap;
+}
+
+.table-view th:first-child,
+.table-view td:first-child {
+ text-align: left;
+}
+
+.table-view thead th {
+ position: sticky;
+ top: 0;
+ background: var(--color-surface);
+ color: var(--color-ink-muted);
+ font-weight: 700;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .bar {
+ transition: none;
+ }
+}
diff --git a/docs/styles/page.css b/docs/styles/page.css
new file mode 100644
index 0000000..8684ad7
--- /dev/null
+++ b/docs/styles/page.css
@@ -0,0 +1,343 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ background: var(--color-plane);
+ color: var(--color-ink);
+ font-family: var(--font-text);
+ font-size: var(--text-base);
+ line-height: 1.55;
+}
+
+a {
+ color: inherit;
+ text-decoration-color: var(--color-honey);
+ text-decoration-thickness: 2px;
+ text-underline-offset: 0.2em;
+}
+
+a:hover {
+ color: var(--color-honey);
+}
+
+:focus-visible {
+ outline: 2px solid var(--color-honey);
+ outline-offset: 3px;
+}
+
+.masthead,
+main,
+.colophon {
+ max-width: var(--page-width);
+ margin-inline: auto;
+ padding-inline: var(--space-gutter);
+}
+
+/* Masthead: a wide headline over a honeycomb wash */
+.masthead {
+ position: relative;
+ padding-block: var(--space-section) calc(var(--space-section) / 2);
+}
+
+.masthead::before {
+ content: "";
+ position: absolute;
+ inset: 0 0 auto auto;
+ width: min(46vw, 34rem);
+ height: 100%;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='97' viewBox='0 0 56 97'%3E%3Cpath d='M28 0l28 16v33L28 65 0 49V16zM28 65l28 16v16H0V81z' fill='none' stroke='%23b8740a' stroke-opacity='.28'/%3E%3C/svg%3E");
+ mask-image: linear-gradient(to left, #000, transparent);
+ pointer-events: none;
+}
+
+.masthead-kicker {
+ margin: 0 0 1rem;
+ color: var(--color-honey);
+ font-size: var(--text-small);
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+h1 {
+ position: relative;
+ max-width: 14ch;
+ margin: 0;
+ font-family: var(--font-display);
+ font-size: var(--text-hero);
+ font-weight: 500;
+ letter-spacing: -0.02em;
+ line-height: 0.98;
+}
+
+.masthead-lede {
+ position: relative;
+ max-width: var(--measure);
+ margin: 1.75rem 0 0;
+ color: var(--color-ink-soft);
+ font-size: var(--text-lede);
+}
+
+.notice {
+ position: relative;
+ max-width: var(--measure);
+ margin: 1.5rem 0 0;
+ padding: 0.75rem 1rem;
+ border-left: 4px solid var(--color-notice-rule);
+ background: var(--color-notice);
+ font-size: var(--text-small);
+}
+
+/* Controls: one row above everything it scopes */
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem 2rem;
+ margin-inline: calc(var(--space-gutter) * -0.5);
+ padding: 0.85rem calc(var(--space-gutter) * 0.5);
+ border-block: 1px solid var(--color-rule);
+ background: color-mix(in srgb, var(--color-plane) 92%, transparent);
+ backdrop-filter: blur(10px);
+}
+
+.controls-group {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ gap: 0.75rem 1.25rem;
+ min-width: 0;
+ margin: 0;
+ padding: 0;
+ border: 0;
+}
+
+.controls-group legend {
+ padding: 0;
+ color: var(--color-ink-muted);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+}
+
+.field {
+ display: grid;
+ gap: 0.25rem;
+ font-size: var(--text-small);
+ color: var(--color-ink-soft);
+}
+
+.field-check {
+ grid-auto-flow: column;
+ align-items: center;
+ gap: 0.5rem;
+ padding-bottom: 0.45rem;
+}
+
+.field input[type="number"],
+.field select {
+ min-height: 2.25rem;
+ padding: 0.3rem 0.6rem;
+ border: 1px solid var(--color-rule);
+ border-radius: var(--radius-control);
+ background: var(--color-surface);
+ color: var(--color-ink);
+ font: inherit;
+}
+
+.field input[type="number"] {
+ width: 6.5rem;
+}
+
+.field input[type="range"] {
+ width: 9rem;
+ accent-color: var(--color-honey);
+}
+
+.field output {
+ color: var(--color-ink);
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+}
+
+.run-button {
+ min-height: 2.25rem;
+ padding: 0.35rem 1.15rem;
+ border: 1px solid var(--color-ink);
+ border-radius: var(--radius-control);
+ background: var(--color-ink);
+ color: var(--color-plane);
+ font: inherit;
+ font-size: var(--text-small);
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ cursor: pointer;
+ transition:
+ transform var(--duration-fast) var(--ease-out),
+ background-color var(--duration-fast) var(--ease-out);
+}
+
+.run-button:hover {
+ background: var(--color-honey);
+ border-color: var(--color-honey);
+}
+
+.run-button:active {
+ transform: translateY(1px);
+}
+
+.run-button:disabled {
+ opacity: 0.55;
+ cursor: progress;
+}
+
+.status {
+ min-height: 1.5rem;
+ margin: 1rem 0 0;
+ color: var(--color-ink-muted);
+ font-size: var(--text-small);
+}
+
+.status[data-state="failed"] {
+ color: var(--diverging-negative);
+ font-weight: 600;
+}
+
+/* Report */
+.report {
+ min-height: 60vh;
+ transition: opacity var(--duration-normal) var(--ease-out);
+}
+
+.report[aria-busy="true"] {
+ opacity: 0.45;
+}
+
+.chapter {
+ display: grid;
+ gap: 1.5rem var(--space-gutter);
+ padding-block: calc(var(--space-section) / 2);
+ border-top: 1px solid var(--color-rule);
+ animation: rise var(--duration-normal) var(--ease-out) both;
+}
+
+.chapter:first-child {
+ border-top: 0;
+}
+
+.chapter-heading {
+ display: grid;
+ gap: 0.5rem;
+ align-content: start;
+}
+
+.chapter-number {
+ color: var(--color-honey);
+ font-family: var(--font-display);
+ font-size: var(--text-section);
+ font-style: italic;
+ line-height: 1;
+}
+
+.chapter h2 {
+ margin: 0;
+ font-family: var(--font-display);
+ font-size: var(--text-section);
+ font-weight: 500;
+ letter-spacing: -0.01em;
+ line-height: 1.05;
+}
+
+.chapter-heading p {
+ max-width: 38ch;
+ margin: 0;
+ color: var(--color-ink-soft);
+}
+
+.chapter-body {
+ display: grid;
+ gap: var(--space-card);
+ min-width: 0;
+}
+
+.field-invite {
+ display: grid;
+ gap: 1rem;
+ justify-items: start;
+}
+
+.field-invite p {
+ max-width: var(--measure);
+ margin: 0;
+ color: var(--color-ink-soft);
+}
+
+.pair-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font-size: var(--text-small);
+}
+
+.pair-list li {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ padding-block: 0.4rem;
+ border-bottom: 1px solid var(--color-rule);
+}
+
+.pair-list strong {
+ font-variant-numeric: tabular-nums;
+}
+
+.colophon {
+ padding-block: 2rem var(--space-section);
+ border-top: 1px solid var(--color-rule);
+ color: var(--color-ink-muted);
+ font-size: var(--text-small);
+}
+
+@media (min-width: 64rem) {
+ .chapter {
+ grid-template-columns: minmax(0, 5fr) minmax(0, 11fr);
+ }
+
+ .controls {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ }
+
+ .chapter-heading {
+ position: sticky;
+ top: 6.5rem;
+ }
+
+ .chapter-body.is-split {
+ grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
+ align-items: start;
+ }
+}
+
+@keyframes rise {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .chapter {
+ animation: none;
+ }
+
+ .report,
+ .run-button {
+ transition: none;
+ }
+}
diff --git a/docs/styles/tokens.css b/docs/styles/tokens.css
new file mode 100644
index 0000000..8c9a5f6
--- /dev/null
+++ b/docs/styles/tokens.css
@@ -0,0 +1,90 @@
+:root {
+ color-scheme: light;
+
+ /* Page */
+ --color-plane: #f3ede0;
+ --color-surface: #fcfcfb;
+ --color-ink: #17150f;
+ --color-ink-soft: #52514e;
+ --color-ink-muted: #807d75;
+ --color-rule: #d9d2c1;
+ --color-honey: #b8740a;
+ --color-notice: #fff4d6;
+ --color-notice-rule: #d9a21b;
+
+ /* Chart chrome */
+ --chart-grid: #e1e0d9;
+ --chart-axis: #c3c2b7;
+ --chart-neutral: #898781;
+ --chart-emphasis: #17150f;
+
+ /* Categorical series, in fixed order */
+ --series-1: #2a78d6;
+ --series-2: #eb6834;
+ --series-3: #1baf7a;
+ --series-4: #eda100;
+ --series-5: #e87ba4;
+
+ /* Diverging scale */
+ --diverging-positive: #1c5cab;
+ --diverging-negative: #d03b3b;
+ --diverging-neutral: #f0efec;
+
+ /* Type */
+ --font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
+ --font-text: system-ui, -apple-system, "Segoe UI", sans-serif;
+ --text-small: 0.8125rem;
+ --text-base: clamp(1rem, 0.95rem + 0.25vw, 1.0625rem);
+ --text-lede: clamp(1.1rem, 1rem + 0.6vw, 1.35rem);
+ --text-section: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem);
+ --text-hero: clamp(2.5rem, 1.4rem + 5.5vw, 5.5rem);
+ --text-figure: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem);
+
+ /* Space */
+ --space-gutter: clamp(1rem, 0.5rem + 2.5vw, 3rem);
+ --space-section: clamp(3rem, 2rem + 4vw, 6rem);
+ --space-card: clamp(1rem, 0.8rem + 1vw, 1.75rem);
+ --measure: 62ch;
+ --page-width: 76rem;
+
+ /* Shape and motion */
+ --radius-card: 0.375rem;
+ --radius-control: 0.25rem;
+ --shadow-card: 0 1px 0 rgb(23 21 15 / 8%), 0 12px 28px -18px rgb(23 21 15 / 35%);
+ --duration-fast: 150ms;
+ --duration-normal: 320ms;
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ color-scheme: dark;
+
+ --color-plane: #100f0c;
+ --color-surface: #1a1a19;
+ --color-ink: #f0efec;
+ --color-ink-soft: #c3c2b7;
+ --color-ink-muted: #96938a;
+ --color-rule: #34322c;
+ --color-honey: #e0a53a;
+ --color-notice: #2a2413;
+ --color-notice-rule: #c98500;
+
+ --chart-grid: #2c2c2a;
+ --chart-axis: #383835;
+ --chart-neutral: #898781;
+ --chart-emphasis: #f0efec;
+
+ --series-1: #3987e5;
+ --series-2: #d95926;
+ --series-3: #199e70;
+ --series-4: #c98500;
+ --series-5: #d55181;
+
+ --diverging-positive: #5598e7;
+ --diverging-negative: #e66767;
+ --diverging-neutral: #383835;
+
+ --shadow-card: 0 1px 0 rgb(255 255 255 / 6%), 0 14px 30px -20px rgb(0 0 0 / 80%);
+ }
+}
diff --git a/images/climate_impact_summary.png b/images/climate_impact_summary.png
index d32a0dc..86b5d62 100644
Binary files a/images/climate_impact_summary.png and b/images/climate_impact_summary.png differ
diff --git a/images/comparative_temperature_analysis.png b/images/comparative_temperature_analysis.png
index 808a737..86b86c4 100644
Binary files a/images/comparative_temperature_analysis.png and b/images/comparative_temperature_analysis.png differ
diff --git a/images/correlation_analysis.png b/images/correlation_analysis.png
index cb3bcaf..626df77 100644
Binary files a/images/correlation_analysis.png and b/images/correlation_analysis.png differ
diff --git a/images/geographic_distribution.png b/images/geographic_distribution.png
index 636d471..7fb5b4b 100644
Binary files a/images/geographic_distribution.png and b/images/geographic_distribution.png differ
diff --git a/images/migration_patterns_synthesis.png b/images/migration_patterns_synthesis.png
index 4fcc977..0433b62 100644
Binary files a/images/migration_patterns_synthesis.png and b/images/migration_patterns_synthesis.png differ
diff --git a/images/species_honey_decline.png b/images/species_honey_decline.png
index a94672a..00381a0 100644
Binary files a/images/species_honey_decline.png and b/images/species_honey_decline.png differ
diff --git a/images/temperature_migration_trend.png b/images/temperature_migration_trend.png
index 62cf6db..4fc52ef 100644
Binary files a/images/temperature_migration_trend.png and b/images/temperature_migration_trend.png differ
diff --git a/images/ufpr_migration_analysis.png b/images/ufpr_migration_analysis.png
index 5587684..0625ce8 100644
Binary files a/images/ufpr_migration_analysis.png and b/images/ufpr_migration_analysis.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 8b5372b..2e93e21 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,296 +1,46 @@
[build-system]
-requires = ["setuptools>=61.0", "wheel"]
+requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "bee-migration-analysis"
version = "1.0.0"
-authors = [
- {name = "Diego Gomes", email = "diego@example.com"},
-]
-description = "Climate Impact Assessment on Bee Migration Patterns"
+authors = [{ name = "Diego Gomes", email = "diego@example.com" }]
+description = "Climate impact assessment on bee migration patterns in South America"
readme = "README.md"
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 4 - Beta",
- "Intended Audience :: Science/Research",
- "Topic :: Scientific/Engineering :: Bio-Informatics",
- "Topic :: Scientific/Engineering :: Atmospheric Science",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Operating System :: OS Independent",
-]
-keywords = ["bees", "migration", "climate", "analysis", "machine-learning", "geospatial"]
-license = {text = "MIT"}
+requires-python = ">=3.11"
+license = { text = "MIT" }
+keywords = ["bees", "migration", "climate", "analysis", "machine-learning"]
dependencies = [
- "pandas>=1.3.0",
- "numpy>=1.21.0",
- "matplotlib>=3.4.0",
- "seaborn>=0.11.0",
- "scikit-learn>=1.0.0",
- "geopandas>=0.10.0",
- "folium>=0.12.0",
- "plotly>=5.0.0",
- "requests>=2.25.0",
- "scipy>=1.7.0",
- "joblib>=1.0.0",
- "openpyxl>=3.0.0",
- "xlsxwriter>=3.0.0",
+ "numpy>=1.26",
+ "pandas>=2.1",
+ "scikit-learn>=1.4",
+ "matplotlib>=3.8",
+ "seaborn>=0.13",
+ "joblib>=1.3",
]
[project.optional-dependencies]
-dev = [
- "pytest>=7.0.0",
- "pytest-cov>=4.0.0",
- "pytest-mock>=3.8.0",
- "pytest-xdist>=3.0.0",
- "pytest-timeout>=2.1.0",
- "coverage>=6.0.0",
- "black>=22.0.0",
- "flake8>=5.0.0",
- "mypy>=0.991",
- "isort>=5.10.0",
- "bandit>=1.7.0",
- "safety>=2.0.0",
- "pre-commit>=2.20.0",
- "pydocstyle>=6.1.0",
-]
-test = [
- "pytest>=7.0.0",
- "pytest-cov>=4.0.0",
- "pytest-mock>=3.8.0",
- "coverage>=6.0.0",
-]
-docs = [
- "sphinx>=4.0.0",
- "sphinx-rtd-theme>=1.0.0",
- "myst-parser>=0.18.0",
- "sphinx-autodoc-typehints>=1.19.0",
-]
-viz = [
- "plotly>=5.0.0",
- "dash>=2.0.0",
- "streamlit>=1.0.0",
- "bokeh>=2.4.0",
-]
-api = [
- "fastapi>=0.70.0",
- "uvicorn>=0.15.0",
- "pydantic>=1.8.0",
- "python-multipart>=0.0.5",
-]
-notebooks = [
- "jupyter>=1.0.0",
- "ipykernel>=6.0.0",
- "nbconvert>=6.0.0",
- "nbformat>=5.0.0",
-]
-performance = [
- "dask>=2021.0.0",
- "numba>=0.56.0",
- "memory-profiler>=0.60.0",
- "line-profiler>=3.5.0",
-]
+dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.6"]
[project.urls]
-"Homepage" = "https://github.com/diegogo/bee-migration-analysis"
-"Bug Reports" = "https://github.com/diegogo/bee-migration-analysis/issues"
-"Source" = "https://github.com/diegogo/bee-migration-analysis"
-"Documentation" = "https://github.com/diegogo/bee-migration-analysis/blob/main/README.md"
+Homepage = "https://github.com/digomes87/Bees"
+Issues = "https://github.com/digomes87/Bees/issues"
[project.scripts]
-bee-analysis = "bee_migration.cli:main"
-generate-charts = "bee_migration.charts:main"
-run-analysis = "bee_migration.analysis:main"
+bee-migration = "bee_migration.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
-[tool.setuptools.package-data]
-bee_migration = ["data/*.csv", "data/*.json", "templates/*.html", "static/*"]
-
-# Black configuration
-[tool.black]
-line-length = 88
-target-version = ['py38', 'py39', 'py310', 'py311']
-include = '\.pyi?$'
-extend-exclude = '''
-(
- /(
- \.eggs
- | \.git
- | \.hg
- | \.mypy_cache
- | \.tox
- | \.venv
- | _build
- | buck-out
- | build
- | dist
- | migrations
- )/
-)
-'''
-
-# isort configuration
-[tool.isort]
-profile = "black"
-line_length = 88
-multi_line_output = 3
-include_trailing_comma = true
-force_grid_wrap = 0
-use_parentheses = true
-ensure_newline_before_comments = true
-src_paths = ["src", "tests"]
-skip_glob = ["**/migrations/*"]
-
-# mypy configuration
-[tool.mypy]
-python_version = "3.8"
-warn_return_any = true
-warn_unused_configs = true
-disallow_untyped_defs = false
-disallow_incomplete_defs = false
-check_untyped_defs = true
-disallow_untyped_decorators = false
-no_implicit_optional = true
-warn_redundant_casts = true
-warn_unused_ignores = true
-warn_no_return = true
-warn_unreachable = true
-strict_equality = true
-show_error_codes = true
-ignore_missing_imports = true
-
-[[tool.mypy.overrides]]
-module = [
- "geopandas.*",
- "folium.*",
- "plotly.*",
- "seaborn.*",
- "sklearn.*",
- "scipy.*",
- "cartopy.*",
- "shapely.*",
- "fiona.*",
- "pyproj.*",
-]
-ignore_missing_imports = true
-
-# pytest configuration
[tool.pytest.ini_options]
-minversion = "7.0"
-addopts = [
- "--strict-markers",
- "--strict-config",
- "--verbose",
- "--tb=short",
- "--cov=src",
- "--cov-report=term-missing",
- "--cov-report=html:htmlcov",
- "--cov-report=xml",
- "--cov-fail-under=80",
- "--durations=10",
- "--color=yes",
- "-ra",
-]
testpaths = ["tests"]
-python_files = ["test_*.py", "*_test.py"]
-python_classes = ["Test*"]
-python_functions = ["test_*"]
-markers = [
- "slow: marks tests as slow (deselect with '-m \"not slow\"')",
- "integration: marks tests as integration tests",
- "unit: marks tests as unit tests",
- "api: marks tests that require API access",
- "data: marks tests that require data files",
- "ml: marks tests for machine learning components",
- "viz: marks tests for visualization components",
- "geo: marks tests for geospatial analysis",
- "climate: marks tests for climate data analysis",
- "migration: marks tests for migration analysis",
- "performance: marks tests for performance benchmarks",
-]
-filterwarnings = [
- "error",
- "ignore::UserWarning",
- "ignore::DeprecationWarning",
- "ignore::PendingDeprecationWarning",
- "ignore::FutureWarning",
- "ignore:.*GDAL.*:RuntimeWarning",
- "ignore:.*shapely.*:RuntimeWarning",
- "ignore:.*geopandas.*:UserWarning",
-]
+pythonpath = ["src"]
+addopts = "--cov=bee_migration --cov-report=term-missing --cov-fail-under=80"
-# Coverage configuration
-[tool.coverage.run]
-source = ["src"]
-branch = true
-parallel = true
-omit = [
- "*/tests/*",
- "*/test_*",
- "*/__pycache__/*",
- "*/venv/*",
- "*/env/*",
- "*/.venv/*",
- "*/.env/*",
- "*/site-packages/*",
- "*/build/*",
- "*/dist/*",
- "*/.git/*",
- "*/.pytest_cache/*",
- "*/htmlcov/*",
- "*/docs/*",
- "*/notebooks/*",
- "*/examples/*",
- "*/scripts/*",
- "*/data/*",
- "*/images/*",
- "*/results/*",
-]
-
-[tool.coverage.report]
-show_missing = true
-skip_covered = false
-skip_empty = true
-sort = "Cover"
-precision = 2
-exclude_lines = [
- "pragma: no cover",
- "def __repr__",
- "if self\.debug",
- "raise AssertionError",
- "raise NotImplementedError",
- "if 0:",
- "if False:",
- "if __name__ == .__main__.:",
- "@(abc\.)?abstractmethod",
- "if TYPE_CHECKING:",
- "if sys.platform",
- "@deprecated",
- "logger\.",
- "logging\.",
- "pass",
- "\.\.\.",
-]
-
-[tool.coverage.html]
-directory = "htmlcov"
-title = "Bee Migration Analysis Coverage Report"
-show_contexts = true
-
-[tool.coverage.xml]
-output = "coverage.xml"
-
-# Bandit security configuration
-[tool.bandit]
-exclude_dirs = ["tests", "build", "dist"]
-skips = ["B101", "B601"]
+[tool.ruff]
+line-length = 88
+target-version = "py311"
-# Flake8 configuration (in setup.cfg since flake8 doesn't support pyproject.toml yet)
-# See setup.cfg for flake8 configuration
\ No newline at end of file
+[tool.ruff.lint]
+select = ["E", "F", "I", "B", "UP", "N", "SIM"]
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 58e922a..0000000
--- a/pytest.ini
+++ /dev/null
@@ -1,81 +0,0 @@
-[tool:pytest]
-# Pytest configuration for Bee Migration Analysis Project
-
-# Test discovery
-python_files = test_*.py *_test.py
-python_classes = Test*
-python_functions = test_*
-
-# Test directories
-testpaths = tests
-
-# Minimum version
-minversion = 7.0
-
-# Add options
-addopts =
- --strict-markers
- --strict-config
- --verbose
- --tb=short
- --cov=src
- --cov-report=term-missing
- --cov-report=html:htmlcov
- --cov-report=xml
- --cov-fail-under=80
- --durations=10
- --color=yes
- -ra
-
-# Markers
-markers =
- slow: marks tests as slow (deselect with '-m "not slow"')
- integration: marks tests as integration tests
- unit: marks tests as unit tests
- api: marks tests that require API access
- data: marks tests that require data files
- ml: marks tests for machine learning components
- viz: marks tests for visualization components
- geo: marks tests for geospatial analysis
- climate: marks tests for climate data analysis
- migration: marks tests for migration analysis
- performance: marks tests for performance benchmarks
-
-# Warnings
-filterwarnings =
- error
- ignore::UserWarning
- ignore::DeprecationWarning
- ignore::PendingDeprecationWarning
- ignore::FutureWarning
- ignore:.*GDAL.*:RuntimeWarning
- ignore:.*shapely.*:RuntimeWarning
- ignore:.*geopandas.*:UserWarning
-
-# Test output
-console_output_style = progress
-junit_family = xunit2
-
-# Logging
-log_cli = true
-log_cli_level = INFO
-log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
-log_cli_date_format = %Y-%m-%d %H:%M:%S
-
-# Coverage
-cov_config = .coveragerc
-
-# Timeout (in seconds)
-timeout = 300
-timeout_method = thread
-
-# Parallel execution
-# Uncomment to enable parallel test execution
-# -n auto
-# --dist worksteal
-
-# Cache
-cache_dir = .pytest_cache
-
-# Doctest
-doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL
\ No newline at end of file
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 40a130b..a6de4e2 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,61 +1,5 @@
-# Development dependencies for Bee Migration Analysis Project
-
-# Testing
-pytest>=7.0.0
-pytest-cov>=4.0.0
-pytest-mock>=3.10.0
-pytest-xdist>=3.0.0 # Parallel test execution
-
-# Code formatting and linting
-black>=23.0.0
-isort>=5.12.0
-flake8>=6.0.0
-flake8-docstrings>=1.7.0
-flake8-import-order>=0.18.0
-mypy>=1.0.0
-
-# Security
-bandit>=1.7.0
-safety>=2.3.0
-
-# Documentation
-sphinx>=6.0.0
-sphinx-rtd-theme>=1.2.0
-myst-parser>=1.0.0
-
-# Jupyter and notebooks
-jupyter>=1.0.0
-jupyterlab>=3.6.0
-ipykernel>=6.20.0
-nbconvert>=7.0.0
-nbformat>=5.7.0
-
-# Development tools
-pre-commit>=3.0.0
-tox>=4.0.0
-bump2version>=1.0.0
-twine>=4.0.0
-
-# Performance profiling
-memory-profiler>=0.60.0
-line-profiler>=4.0.0
-
-# API development (for future REST API)
-fastapi>=0.95.0
-uvicorn>=0.20.0
-pydantic>=1.10.0
-
-# Dashboard development (for future interactive dashboard)
-streamlit>=1.20.0
-dash>=2.10.0
-plotly-dash>=5.14.0
-
-# Additional data science tools
-jupyter-dash>=0.4.0
-voila>=0.4.0
-
-# Environment management
-python-dotenv>=1.0.0
-
-# Git hooks and automation
-gitpython>=3.1.0
\ No newline at end of file
+# Development dependencies (mirrors the "dev" extra in pyproject.toml)
+-r requirements.txt
+pytest>=8.0
+pytest-cov>=5.0
+ruff>=0.6
diff --git a/requirements.txt b/requirements.txt
index 249dd3f..bb56e4d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,17 +1,7 @@
-# Dependências para Análise de Abelhas e Mudanças Climáticas
-# Projeto: Bee Behavior Analysis
-# Autor: Diego Gomes
-
-# Análise de dados essenciais
-numpy>=1.21.0
-pandas>=1.3.0
-
-# Visualização
-matplotlib>=3.5.0
-seaborn>=0.11.0
-
-# Machine Learning básico
-scikit-learn>=1.0.0
-
-# Utilitários
-requests>=2.25.0
\ No newline at end of file
+# Runtime dependencies (mirrors pyproject.toml)
+numpy>=1.26
+pandas>=2.1
+scikit-learn>=1.4
+matplotlib>=3.8
+seaborn>=0.13
+joblib>=1.3
diff --git a/results/analysis.log b/results/analysis.log
deleted file mode 100644
index 7d61524..0000000
--- a/results/analysis.log
+++ /dev/null
@@ -1,102 +0,0 @@
-2025-08-22 16:05:28,193 - __main__ - INFO - BeeClimateAnalyzer inicializado
-2025-08-22 16:05:28,193 - __main__ - INFO - Iniciando análise completa...
-2025-08-22 16:05:28,193 - __main__ - INFO - Iniciando carregamento de dados...
-2025-08-22 16:05:28,194 - __main__ - INFO - Dados simulados gerados para demonstração
-2025-08-22 16:05:28,194 - __main__ - INFO - Iniciando preprocessamento dos dados...
-2025-08-22 16:05:28,203 - __main__ - INFO - Preprocessamento concluído
-2025-08-22 16:05:28,204 - __main__ - INFO - Dados carregados: 120 registros
-2025-08-22 16:05:28,204 - __main__ - INFO - Iniciando análise de padrões temporais...
-2025-08-22 16:05:28,208 - __main__ - INFO - Análise temporal concluída
-2025-08-22 16:05:28,208 - __main__ - INFO - Iniciando análise de correlações...
-2025-08-22 16:05:28,214 - __main__ - INFO - Análise de correlações concluída
-2025-08-22 16:05:28,214 - __main__ - INFO - Iniciando análise de padrões geográficos...
-2025-08-22 16:05:28,217 - __main__ - INFO - Análise geográfica concluída
-2025-08-22 16:05:28,217 - __main__ - INFO - Gerando relatório resumo...
-2025-08-22 16:05:28,217 - __main__ - INFO - Relatório salvo em: /Users/diegogo/Documents/Analises/Bees/results/analysis_summary_report.txt
-2025-08-22 16:05:28,217 - __main__ - INFO - Salvando resultados da análise...
-2025-08-22 16:05:28,221 - __main__ - INFO - Resultados salvos com sucesso
-2025-08-22 16:05:28,221 - __main__ - INFO - Análise completa finalizada
-2025-08-22 16:05:40,252 - __main__ - INFO - BeeMigrationPredictor inicializado
-2025-08-22 16:05:40,252 - __main__ - INFO - Iniciando pipeline completo de ML...
-2025-08-22 16:05:40,252 - __main__ - INFO - Carregando e preparando dados para ML...
-2025-08-22 16:05:40,253 - bee_analysis - INFO - BeeClimateAnalyzer inicializado
-2025-08-22 16:05:40,253 - bee_analysis - INFO - Iniciando carregamento de dados...
-2025-08-22 16:05:40,254 - bee_analysis - INFO - Dados simulados gerados para demonstração
-2025-08-22 16:05:40,254 - bee_analysis - INFO - Iniciando preprocessamento dos dados...
-2025-08-22 16:05:40,262 - bee_analysis - INFO - Preprocessamento concluído
-2025-08-22 16:05:40,262 - bee_analysis - INFO - Dados carregados: 120 registros
-2025-08-22 16:05:40,262 - __main__ - INFO - Realizando engenharia de features...
-2025-08-22 16:05:40,267 - __main__ - INFO - Dados preparados: 120 registros, 33 features
-2025-08-22 16:05:40,267 - __main__ - INFO - Preparando dados ML para target: bee_abundance
-2025-08-22 16:05:40,267 - __main__ - INFO - Dados ML preparados: 120 amostras, 21 features
-2025-08-22 16:05:40,268 - __main__ - INFO - Dados divididos - Treino: 84, Validação: 18, Teste: 18
-2025-08-22 16:05:40,268 - __main__ - INFO - Modelos inicializados: ['Linear Regression', 'Random Forest', 'Gradient Boosting', 'SVR']
-2025-08-22 16:05:40,268 - __main__ - INFO - Iniciando treinamento dos modelos...
-2025-08-22 16:05:40,268 - __main__ - INFO - Treinando Linear Regression...
-2025-08-22 16:05:40,274 - __main__ - INFO - Treinando Random Forest...
-2025-08-22 16:05:43,127 - __main__ - INFO - Random Forest - Melhores parâmetros: {'max_depth': 10, 'min_samples_split': 2, 'n_estimators': 300}
-2025-08-22 16:05:43,127 - __main__ - INFO - Treinando Gradient Boosting...
-2025-08-22 16:05:43,574 - __main__ - INFO - Gradient Boosting - Melhores parâmetros: {'learning_rate': 0.1, 'max_depth': 3, 'n_estimators': 200}
-2025-08-22 16:05:43,574 - __main__ - INFO - Treinando SVR...
-2025-08-22 16:05:43,604 - __main__ - INFO - SVR - Melhores parâmetros: {'C': 1, 'gamma': 'scale', 'kernel': 'linear'}
-2025-08-22 16:05:43,604 - __main__ - INFO - Treinamento concluído
-2025-08-22 16:05:43,604 - __main__ - INFO - Avaliando performance dos modelos...
-2025-08-22 16:05:43,605 - __main__ - INFO - Linear Regression - Test R²: 0.997, Test RMSE: 0.742
-2025-08-22 16:05:43,646 - __main__ - INFO - Random Forest - Test R²: 0.850, Test RMSE: 4.937
-2025-08-22 16:05:43,648 - __main__ - INFO - Gradient Boosting - Test R²: 0.892, Test RMSE: 4.183
-2025-08-22 16:05:43,649 - __main__ - INFO - SVR - Test R²: 0.995, Test RMSE: 0.882
-2025-08-22 16:05:43,649 - __main__ - INFO - Analisando importância das features...
-2025-08-22 16:06:16,877 - __main__ - INFO - BeeMigrationPredictor inicializado
-2025-08-22 16:06:16,877 - __main__ - INFO - Iniciando pipeline completo de ML...
-2025-08-22 16:06:16,877 - __main__ - INFO - Carregando e preparando dados para ML...
-2025-08-22 16:06:16,878 - bee_analysis - INFO - BeeClimateAnalyzer inicializado
-2025-08-22 16:06:16,878 - bee_analysis - INFO - Iniciando carregamento de dados...
-2025-08-22 16:06:16,878 - bee_analysis - INFO - Dados simulados gerados para demonstração
-2025-08-22 16:06:16,878 - bee_analysis - INFO - Iniciando preprocessamento dos dados...
-2025-08-22 16:06:16,886 - bee_analysis - INFO - Preprocessamento concluído
-2025-08-22 16:06:16,887 - bee_analysis - INFO - Dados carregados: 120 registros
-2025-08-22 16:06:16,887 - __main__ - INFO - Realizando engenharia de features...
-2025-08-22 16:06:16,891 - __main__ - INFO - Dados preparados: 120 registros, 33 features
-2025-08-22 16:06:16,891 - __main__ - INFO - Preparando dados ML para target: bee_abundance
-2025-08-22 16:06:16,891 - __main__ - INFO - Dados ML preparados: 120 amostras, 21 features
-2025-08-22 16:06:16,892 - __main__ - INFO - Dados divididos - Treino: 84, Validação: 18, Teste: 18
-2025-08-22 16:06:16,892 - __main__ - INFO - Modelos inicializados: ['Linear Regression', 'Random Forest', 'Gradient Boosting', 'SVR']
-2025-08-22 16:06:16,892 - __main__ - INFO - Iniciando treinamento dos modelos...
-2025-08-22 16:06:16,892 - __main__ - INFO - Treinando Linear Regression...
-2025-08-22 16:06:16,892 - __main__ - INFO - Treinando Random Forest...
-2025-08-22 16:06:19,720 - __main__ - INFO - Random Forest - Melhores parâmetros: {'max_depth': 10, 'min_samples_split': 2, 'n_estimators': 300}
-2025-08-22 16:06:19,720 - __main__ - INFO - Treinando Gradient Boosting...
-2025-08-22 16:06:20,145 - __main__ - INFO - Gradient Boosting - Melhores parâmetros: {'learning_rate': 0.1, 'max_depth': 3, 'n_estimators': 200}
-2025-08-22 16:06:20,145 - __main__ - INFO - Treinando SVR...
-2025-08-22 16:06:20,174 - __main__ - INFO - SVR - Melhores parâmetros: {'C': 1, 'gamma': 'scale', 'kernel': 'linear'}
-2025-08-22 16:06:20,174 - __main__ - INFO - Treinamento concluído
-2025-08-22 16:06:20,174 - __main__ - INFO - Avaliando performance dos modelos...
-2025-08-22 16:06:20,175 - __main__ - INFO - Linear Regression - Test R²: 0.997, Test RMSE: 0.742
-2025-08-22 16:06:20,228 - __main__ - INFO - Random Forest - Test R²: 0.850, Test RMSE: 4.937
-2025-08-22 16:06:20,230 - __main__ - INFO - Gradient Boosting - Test R²: 0.892, Test RMSE: 4.183
-2025-08-22 16:06:20,231 - __main__ - INFO - SVR - Test R²: 0.995, Test RMSE: 0.882
-2025-08-22 16:06:20,231 - __main__ - INFO - Analisando importância das features...
-2025-08-22 16:06:20,263 - __main__ - INFO - Análise de importância concluída
-2025-08-22 16:06:20,263 - __main__ - INFO - Gerando relatório de predições...
-2025-08-22 16:06:20,263 - __main__ - INFO - Relatório de predições salvo em: /Users/diegogo/Documents/Analises/Bees/results/ml_predictions_report.txt
-2025-08-22 16:06:20,263 - __main__ - INFO - Salvando modelos treinados...
-2025-08-22 16:06:20,316 - __main__ - INFO - Modelos salvos em: /Users/diegogo/Documents/Analises/Bees/results/models
-2025-08-22 16:06:20,316 - __main__ - INFO - Pipeline de ML concluído
-2025-08-22 16:11:49,597 - __main__ - INFO - Gerando relatório abrangente de migração
-2025-08-22 16:11:49,598 - __main__ - INFO - Iniciando análise baseada no estudo da UFPR
-2025-08-22 16:11:50,832 - __main__ - INFO - Gráfico de migração UFPR salvo em: /Users/diegogo/Documents/Analises/Bees/images/ufpr_migration_analysis.png
-2025-08-22 16:11:50,832 - __main__ - INFO - Analisando insights do dataset Dryad
-2025-08-22 16:11:50,832 - __main__ - INFO - Analisando impactos globais do aquecimento
-2025-08-22 16:11:50,832 - __main__ - INFO - Relatório abrangente salvo em: /Users/diegogo/Documents/Analises/Bees/results/bee_migration_comprehensive_report.txt
-2025-08-22 16:14:12,270 - __main__ - INFO - Dados processados carregados: 120 registros
-2025-08-22 16:14:12,270 - __main__ - INFO - Comparando tendências de temperatura
-2025-08-22 16:14:12,885 - __main__ - INFO - Análise comparativa de temperatura salva em: /Users/diegogo/Documents/Analises/Bees/images/comparative_temperature_analysis.png
-2025-08-22 16:14:12,885 - __main__ - INFO - Analisando padrões de migração integrados
-2025-08-22 16:14:12,899 - __main__ - ERROR - Erro na análise comparativa: x and y must have same first dimension, but have shapes (9,) and (10,)
-2025-08-22 16:14:29,651 - __main__ - INFO - Dados processados carregados: 120 registros
-2025-08-22 16:14:29,652 - __main__ - INFO - Comparando tendências de temperatura
-2025-08-22 16:14:30,241 - __main__ - INFO - Análise comparativa de temperatura salva em: /Users/diegogo/Documents/Analises/Bees/images/comparative_temperature_analysis.png
-2025-08-22 16:14:30,241 - __main__ - INFO - Analisando padrões de migração integrados
-2025-08-22 16:14:30,772 - __main__ - INFO - Análise de padrões de migração salva em: /Users/diegogo/Documents/Analises/Bees/images/migration_patterns_synthesis.png
-2025-08-22 16:14:30,772 - __main__ - INFO - Gerando relatório integrado
-2025-08-22 16:14:30,773 - __main__ - INFO - Relatório integrado salvo em: /Users/diegogo/Documents/Analises/Bees/results/integrated_migration_report.txt
diff --git a/results/analysis_summary_report.txt b/results/analysis_summary_report.txt
index 8808b54..be29341 100644
--- a/results/analysis_summary_report.txt
+++ b/results/analysis_summary_report.txt
@@ -1,50 +1,36 @@
================================================================================
-RELATÓRIO DE ANÁLISE: ABELHAS E MUDANÇAS CLIMÁTICAS
+ANALYSIS REPORT: BEES AND CLIMATE CHANGE
================================================================================
-Data da análise: 22/08/2025 16:05
-Período analisado: 2000 - 2023
-Regiões analisadas: Norte, Nordeste, Centro-Oeste, Sudeste, Sul
-Total de registros: 120
-RESUMO DOS DADOS
-----------------------------------------
-Temperatura média: 25.0°C
-Precipitação média: 1383mm
-Abundância média de abelhas: 60.9
-Diversidade média de espécies: 34.0
-Produção média de mel: 23.1kg
+Period analysed: 2000 - 2023
+Regions analysed: Center-West, North, Northeast, South, Southeast
+Total records: 120
-TENDÊNCIAS PRINCIPAIS
+DATA SUMMARY
----------------------------------------
-Norte:
- - Temperatura: +0.083°C/ano
- - Abundância de abelhas: -0.69/ano
-Nordeste:
- - Temperatura: +0.123°C/ano
- - Abundância de abelhas: -0.80/ano
-Centro-Oeste:
- - Temperatura: +0.129°C/ano
- - Abundância de abelhas: -1.00/ano
-Sudeste:
- - Temperatura: +0.116°C/ano
- - Abundância de abelhas: -1.04/ano
-Sul:
- - Temperatura: +0.131°C/ano
- - Abundância de abelhas: -0.78/ano
+Mean temperature: 25.0 °C
+Mean precipitation: 1379 mm
+Mean bee abundance: 61.1
+Mean species diversity: 34.0
+Mean honey production: 22.8 kg
-CORRELAÇÕES SIGNIFICATIVAS
+REGIONAL TRENDS
----------------------------------------
-bee_abundance ↔ bee_health_index: 0.976 (strong)
-species_diversity ↔ bee_health_index: 0.877 (strong)
-bee_abundance ↔ species_diversity: 0.821 (strong)
-precipitation ↔ honey_production: 0.625 (moderate)
-precipitation ↔ bee_health_index: 0.619 (moderate)
+Center-West: temperature +0.148 °C/year, bee abundance -0.54/year
+North: temperature +0.112 °C/year, bee abundance -0.74/year
+Northeast: temperature +0.091 °C/year, bee abundance -1.01/year
+South: temperature +0.117 °C/year, bee abundance -1.15/year
+Southeast: temperature +0.108 °C/year, bee abundance -0.91/year
-CONCLUSÕES PRINCIPAIS
+STRONGEST CORRELATIONS
----------------------------------------
-1. Observa-se tendência de aquecimento em todas as regiões
-2. Declínio geral na abundância de abelhas correlacionado com temperatura
-3. Variações regionais significativas nos padrões de mudança
-4. Necessidade de monitoramento contínuo e medidas de conservação
+bee_abundance <-> bee_health_index: +0.977 (strong)
+species_diversity <-> bee_health_index: +0.872 (strong)
+bee_abundance <-> species_diversity: +0.819 (strong)
+precipitation <-> bee_health_index: +0.573 (moderate)
+precipitation <-> honey_production: +0.564 (moderate)
-================================================================================
\ No newline at end of file
+KEY FINDINGS
+----------------------------------------
+1. Warming in 5 of 5 regions
+2. Bee abundance declining in 5 of 5 regions
diff --git a/results/correlation_analysis_correlation_matrix.csv b/results/correlation_analysis_correlation_matrix.csv
index afe9491..b4c8c04 100644
--- a/results/correlation_analysis_correlation_matrix.csv
+++ b/results/correlation_analysis_correlation_matrix.csv
@@ -1,10 +1,10 @@
-,temperature,precipitation,temperature_anomaly,precipitation_anomaly,climate_stress_index,bee_abundance,species_diversity,honey_production,bee_health_index
-temperature,1.0,0.04541666110904529,0.3533531450800576,-0.09532244207573382,-0.008894534698899042,0.3175869027503415,0.34816263240858986,-0.47231861627266514,0.23115310594102986
-precipitation,0.04541666110904529,1.0,-0.057332151944656164,0.23063917734203157,0.0520307106915476,0.526008638224973,0.523506043404646,0.6248329023715301,0.6191099750685841
-temperature_anomaly,0.3533531450800576,-0.057332151944656164,1.0,-0.255517429630879,0.09951119703024762,-0.43146685300590515,-0.3978967608628842,-0.38027299610257825,-0.47773011886059713
-precipitation_anomaly,-0.09532244207573382,0.23063917734203157,-0.255517429630879,1.0,-0.013792400470429567,0.2010553031747248,0.1985120682332687,0.26710825958236173,0.2417907174526581
-climate_stress_index,-0.008894534698899042,0.0520307106915476,0.09951119703024762,-0.013792400470429567,1.0,-0.09568803224944812,-0.05590259055277858,-0.002842643805394284,-0.08200739623407755
-bee_abundance,0.3175869027503415,0.526008638224973,-0.43146685300590515,0.2010553031747248,-0.09568803224944812,1.0,0.82148382744784,0.2852576114017572,0.9763808773591254
-species_diversity,0.34816263240858986,0.523506043404646,-0.3978967608628842,0.1985120682332687,-0.05590259055277858,0.82148382744784,1.0,0.20273421805229888,0.8772837522089582
-honey_production,-0.47231861627266514,0.6248329023715301,-0.38027299610257825,0.26710825958236173,-0.002842643805394284,0.2852576114017572,0.20273421805229888,1.0,0.43207164658651676
-bee_health_index,0.23115310594102986,0.6191099750685841,-0.47773011886059713,0.2417907174526581,-0.08200739623407755,0.9763808773591254,0.8772837522089582,0.43207164658651676,1.0
+,temperature,precipitation,bee_abundance,species_diversity,honey_production,temperature_anomaly,precipitation_anomaly,climate_stress_index,bee_health_index
+temperature,1.0,0.001624751734940893,0.330104688185209,0.41222980122321196,-0.39590669843547555,0.3628642641749675,-0.10807261982920498,-0.008441079437772761,0.2623649547525306
+precipitation,0.001624751734940893,1.0,0.48075974317234754,0.5101753642944048,0.564330743253366,-0.07235202531873104,0.24292891641140232,0.013342888364203326,0.5729529142872981
+bee_abundance,0.330104688185209,0.48075974317234754,1.0,0.8194918935935008,0.3574740652533684,-0.4328248893870115,0.2479515695431243,0.10662502056283835,0.9767241485419894
+species_diversity,0.41222980122321196,0.5101753642944048,0.8194918935935008,1.0,0.22803902237672777,-0.3424898310178891,0.19462793061639008,0.06499784467288801,0.8724213429253781
+honey_production,-0.39590669843547555,0.564330743253366,0.3574740652533684,0.22803902237672777,1.0,-0.40527910405450457,0.14960810760951104,0.07368070720763355,0.49608869388832905
+temperature_anomaly,0.3628642641749675,-0.07235202531873104,-0.4328248893870115,-0.3424898310178891,-0.40527910405450457,1.0,-0.29783208350628126,-0.1381638757182439,-0.4666100640489673
+precipitation_anomaly,-0.10807261982920498,0.24292891641140232,0.2479515695431243,0.19462793061639008,0.14960810760951104,-0.29783208350628126,1.0,0.05115032864374663,0.25141914565269735
+climate_stress_index,-0.008441079437772761,0.013342888364203326,0.10662502056283835,0.06499784467288801,0.07368070720763355,-0.1381638757182439,0.05115032864374663,1.0,0.10507688891369779
+bee_health_index,0.2623649547525306,0.5729529142872981,0.9767241485419894,0.8724213429253781,0.49608869388832905,-0.4666100640489673,0.25141914565269735,0.10507688891369779,1.0
diff --git a/results/feature_importance.csv b/results/feature_importance.csv
index 8cbb74f..ceb5448 100644
--- a/results/feature_importance.csv
+++ b/results/feature_importance.csv
@@ -1,22 +1,19 @@
,Linear Regression,Random Forest,Gradient Boosting,SVR
-year_normalized,0.1463621610036389,0.03896525557540437,0.042112046837058464,0.20703407862063639
-latitude,6.046847654230619,0.019302287612705905,0.002569051983040945,0.04991176300670652
-longitude,0.08267074362588635,0.1104405580046793,0.06761834663939043,0.02372760394252338
-temperature,22.212380811928277,0.003933256199611833,0.00012840997220854824,0.024502274328632723
-precipitation,9.959506703205838,0.01486400734576359,0.005131252559944259,0.11899175121626904
-temperature_anomaly,1.9879159548616525,0.007809765838043586,0.000681224070475502,0.18779226096979773
-precipitation_anomaly,0.14109987592846404,0.0046223518431690504,0.00010391663833123426,0.0013024645935304502
-climate_stress_index,0.80744832692302,0.0052934055395319375,0.0012939275020794247,0.05297144744785909
-temp_precip_ratio,1.6778571955351111,0.012741377429456444,0.009414557263655244,0.04143732050031668
-climate_extremity,0.8105576376783341,0.004625960713261949,0.0004603606135955526,0.030597584296427005
-temp_lat_interaction,4.045098145521923,0.04418185989328133,0.031877234094144315,0.33980266930044145
-precip_lat_interaction,2.8679117616355505,0.00798234020749373,0.0012083643052002474,0.5182926028391055
-region_encoded,1.104895894687535,0.001633021930632412,8.449739967571782e-05,0.07900186319358005
-temperature_lag1,24.811525862940883,0.009367807395484535,0.0024773215568039287,0.029536703570762768
-temperature_change,5.454648733386253,0.011444008355672518,0.006139226341427193,0.016306451664797272
-precipitation_lag1,7.576920075662478,0.037830818183525815,0.017652167690354777,0.21790157752299777
-precipitation_change,2.339138132629724,0.00459179580297482,0.0007488515081492604,0.03190307397437042
-bee_abundance_lag1,11.836191871750014,0.5708426421101925,0.660623539034064,11.492678353726395
-bee_abundance_change,6.34589920224737,0.08026742079699074,0.14816502061278858,6.4847975403973335
-temperature_zscore,0.9586600083242681,0.005070360198108699,0.0002251656392099291,0.16816034850142092
-precipitation_zscore,0.15649161861004796,0.004189699024014957,0.0012855177384024266,0.001911310186648807
+year_normalized,0.5654505757484539,0.0813036250417756,0.18687328120962682,0.5270293561181866
+latitude,0.9390086640365343,0.021514701510010226,0.011695010264027339,0.020792330625974097
+longitude,0.022390564400903135,0.07835256412954159,0.11284475075441232,0.005478175305066069
+temperature,0.0025207550518666078,0.01840208804710155,0.034797420069551256,0.002786234788270081
+precipitation,0.1335645420694197,0.008980569384462279,0.00518742280927943,0.04348278356958525
+temperature_precipitation_ratio,0.004763924996907987,0.020507828963201948,0.012220581610052461,0.0048284729276501515
+temperature_by_latitude,0.059371677715027195,0.009897870640850192,0.0011826899544929658,0.01759573580598659
+precipitation_by_latitude,1.8830399204612427,0.009570511266596216,0.0038280850198038397,-0.00011779729677221517
+temperature_previous_year,0.003741658321199459,0.016197447583382686,0.01648212281077329,0.0012293230109233777
+precipitation_previous_year,0.11146116602441467,0.06256382626388304,0.1483514454160883,0.0411458551147066
+temperature_change,0.0016048383251455079,0.02406246535300387,0.014615540240546376,0.004411185033119658
+precipitation_change,0.014526399742355933,0.030049183270410305,0.026127895917910315,0.0005311956095188419
+region_Center-West,0.05138054479568742,0.0002896449275355262,0.0,0.04761745219333008
+region_North,2.2240112207691456,0.0010478392865270081,0.0,0.1082025284897615
+region_Northeast,0.13713008747452343,3.085337827263279e-05,0.0,0.06440762566102409
+region_South,1.0383174800966075,0.0004988790613438265,1.3884532099617265e-05,0.13801303688262276
+region_Southeast,0.10413058705180922,0.00209719545611291,0.002069820844102599,0.0029891154982002012
+bee_abundance_previous_year,0.01801351526663434,0.7096363978289075,0.3293032612631944,0.0026806367574275457
diff --git a/results/ml_predictions_report.txt b/results/ml_predictions_report.txt
index 6274a4d..fd89e96 100644
--- a/results/ml_predictions_report.txt
+++ b/results/ml_predictions_report.txt
@@ -1,49 +1,45 @@
================================================================================
-RELATÓRIO DE PREDIÇÕES - MACHINE LEARNING
+PREDICTION REPORT: MACHINE LEARNING
================================================================================
-Data: 22/08/2025 16:06
-Modelos treinados: 4
-Features utilizadas: 21
-PERFORMANCE DOS MODELOS
-----------------------------------------
-Linear Regression ⭐:
- Test R²: 0.9966
- Test RMSE: 0.7425
- Test MAE: 0.6081
- Test MAPE: 1.03%
+Models trained: 4
+PERFORMANCE ON HELD-OUT YEARS
+----------------------------------------
+Linear Regression:
+ Test R2: 0.8078
+ Test RMSE: 5.1513
+ Test MAE: 3.7771
+ Test MAPE: 7.56%
Random Forest:
- Test R²: 0.8497
- Test RMSE: 4.9366
- Test MAE: 3.7414
- Test MAPE: 7.21%
-
+ Test R2: 0.5341
+ Test RMSE: 8.0203
+ Test MAE: 6.7199
+ Test MAPE: 15.00%
Gradient Boosting:
- Test R²: 0.8921
- Test RMSE: 4.1826
- Test MAE: 3.3490
- Test MAPE: 6.32%
-
+ Test R2: 0.4782
+ Test RMSE: 8.4873
+ Test MAE: 7.1184
+ Test MAPE: 15.64%
SVR:
- Test R²: 0.9952
- Test RMSE: 0.8822
- Test MAE: 0.3255
- Test MAPE: 0.49%
+ Test R2: 0.7765
+ Test RMSE: 5.5552
+ Test MAE: 4.3616
+ Test MAPE: 8.67%
-FEATURES MAIS IMPORTANTES
+MOST IMPORTANT FEATURES (Linear Regression)
----------------------------------------
- 2. temperature: 22.2124
- 4. precipitation: 9.9595
- 7. latitude: 6.0468
- 9. temp_lat_interaction: 4.0451
-10. precip_lat_interaction: 2.8679
+ region_North: 2.2240
+ precipitation_by_latitude: 1.8830
+ region_South: 1.0383
+ latitude: 0.9390
+ year_normalized: 0.5655
+ region_Northeast: 0.1371
+ precipitation: 0.1336
+ precipitation_previous_year: 0.1115
+ region_Southeast: 0.1041
+ temperature_by_latitude: 0.0594
-RECOMENDAÇÕES
+RECOMMENDATION
----------------------------------------
-1. Melhor modelo: Linear Regression
-2. R² de 0.997 indica boa capacidade preditiva
-3. Considerar coleta de mais dados para melhorar performance
-4. Monitorar predições em dados futuros para validação
-
-================================================================================
\ No newline at end of file
+Best model: Linear Regression
diff --git a/results/model_metrics.csv b/results/model_metrics.csv
index 99db89f..5a57e83 100644
--- a/results/model_metrics.csv
+++ b/results/model_metrics.csv
@@ -1,5 +1,5 @@
-,train_rmse,train_mae,train_r2,train_mape,validation_rmse,validation_mae,validation_r2,validation_mape,test_rmse,test_mae,test_r2,test_mape
-Linear Regression,1.352359999446932,0.6807841144044147,0.9871140154546354,1.089932324379743,1.2252998763463085,0.7012906615430157,0.9848628916964628,1.1280044413030776,0.7424816796962515,0.6081005174025869,0.996599667929111,1.0312121920072226
-Random Forest,1.8179175212532526,1.4035617331543908,0.9767147275455272,2.431562974576743,5.778698185932339,4.4049777563791555,0.663319344777719,8.213707314685267,4.936557048858978,3.741428249016578,0.8496864375296062,7.209039963061152
-Gradient Boosting,0.04222641592367835,0.033685408045474166,0.9999874367669745,0.05512111815987138,4.290087226946971,3.058841093317145,0.8144374278688539,5.847010110952831,4.182574920100003,3.3490260303388735,0.8920960593893889,6.324308737961615
-SVR,1.5833005600117984,0.3698789875905164,0.9823371938019361,0.5669124754761595,1.3644145948552504,0.477790633317528,0.9812305800450524,0.7034787629207675,0.882190079146761,0.3255484884075336,0.9951996358208818,0.49496287031838326
+,train_rmse,train_mae,train_r2,train_mape,test_rmse,test_mae,test_r2,test_mape
+Linear Regression,4.169336970913084,3.362506991085017,0.8239157855129893,5.574998092873463,5.151310034005058,3.777065624865094,0.8077846424232408,7.558373373339333
+Random Forest,2.1858694225480173,1.7639619083089093,0.9516011702773275,2.896288663782916,8.020270025142755,6.719946602612251,0.5340592582894774,14.99515914064869
+Gradient Boosting,0.7957525105249122,0.6527676312541921,0.9935858004693002,1.062680832782898,8.487309760684164,7.1184477521360865,0.4782135320745009,15.637017276982665
+SVR,4.4305674002287,3.3764779888478063,0.8011593719944766,5.58648261856152,5.555164263715911,4.361619323042943,0.776464493339675,8.673854130835252
diff --git a/results/models/gradient_boosting_model.pkl b/results/models/gradient_boosting_model.pkl
deleted file mode 100644
index 7f9cf06..0000000
Binary files a/results/models/gradient_boosting_model.pkl and /dev/null differ
diff --git a/results/models/linear_regression_model.pkl b/results/models/linear_regression_model.pkl
deleted file mode 100644
index 8fed559..0000000
Binary files a/results/models/linear_regression_model.pkl and /dev/null differ
diff --git a/results/models/random_forest_model.pkl b/results/models/random_forest_model.pkl
deleted file mode 100644
index 9c88b2d..0000000
Binary files a/results/models/random_forest_model.pkl and /dev/null differ
diff --git a/results/models/scaler.pkl b/results/models/scaler.pkl
deleted file mode 100644
index 40ae4c3..0000000
Binary files a/results/models/scaler.pkl and /dev/null differ
diff --git a/results/models/svr_model.pkl b/results/models/svr_model.pkl
deleted file mode 100644
index 328d32e..0000000
Binary files a/results/models/svr_model.pkl and /dev/null differ
diff --git a/results/region_encoder.pkl b/results/region_encoder.pkl
deleted file mode 100644
index 95ba556..0000000
Binary files a/results/region_encoder.pkl and /dev/null differ
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index a232281..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,222 +0,0 @@
-[metadata]
-name = bee-migration-analysis
-version = 1.0.0
-author = Diego Gomes
-author_email = diego@example.com
-description = Climate Impact Assessment on Bee Migration Patterns
-long_description = file: README.md
-long_description_content_type = text/markdown
-url = https://github.com/diegogo/bee-migration-analysis
-project_urls =
- Bug Reports = https://github.com/diegogo/bee-migration-analysis/issues
- Source = https://github.com/diegogo/bee-migration-analysis
- Documentation = https://github.com/diegogo/bee-migration-analysis/blob/main/README.md
-classifiers =
- Development Status :: 4 - Beta
- Intended Audience :: Science/Research
- Topic :: Scientific/Engineering :: Bio-Informatics
- Topic :: Scientific/Engineering :: Atmospheric Science
- License :: OSI Approved :: MIT License
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
- Programming Language :: Python :: 3.11
- Operating System :: OS Independent
- Environment :: Console
- Natural Language :: English
-keywords = bees, migration, climate, analysis, machine-learning, geospatial
-license = MIT
-license_files = LICENSE
-
-[options]
-packages = find:
-package_dir =
- = src
-python_requires = >=3.8
-include_package_data = True
-zip_safe = False
-install_requires =
- pandas>=1.3.0
- numpy>=1.21.0
- matplotlib>=3.4.0
- seaborn>=0.11.0
- scikit-learn>=1.0.0
- geopandas>=0.10.0
- folium>=0.12.0
- plotly>=5.0.0
- requests>=2.25.0
- scipy>=1.7.0
- joblib>=1.0.0
- openpyxl>=3.0.0
- xlsxwriter>=3.0.0
-
-[options.packages.find]
-where = src
-
-[options.extras_require]
-dev =
- pytest>=7.0.0
- pytest-cov>=4.0.0
- pytest-mock>=3.8.0
- pytest-xdist>=3.0.0
- pytest-timeout>=2.1.0
- coverage>=6.0.0
- black>=22.0.0
- flake8>=5.0.0
- mypy>=0.991
- isort>=5.10.0
- bandit>=1.7.0
- safety>=2.0.0
- pre-commit>=2.20.0
- pydocstyle>=6.1.0
-test =
- pytest>=7.0.0
- pytest-cov>=4.0.0
- pytest-mock>=3.8.0
- coverage>=6.0.0
-docs =
- sphinx>=4.0.0
- sphinx-rtd-theme>=1.0.0
- myst-parser>=0.18.0
- sphinx-autodoc-typehints>=1.19.0
-viz =
- plotly>=5.0.0
- dash>=2.0.0
- streamlit>=1.0.0
- bokeh>=2.4.0
-api =
- fastapi>=0.70.0
- uvicorn>=0.15.0
- pydantic>=1.8.0
- python-multipart>=0.0.5
-notebooks =
- jupyter>=1.0.0
- ipykernel>=6.0.0
- nbconvert>=6.0.0
- nbformat>=5.0.0
-performance =
- dask>=2021.0.0
- numba>=0.56.0
- memory-profiler>=0.60.0
- line-profiler>=3.5.0
-
-[options.entry_points]
-console_scripts =
- bee-analysis = bee_migration.cli:main
- generate-charts = bee_migration.charts:main
- run-analysis = bee_migration.analysis:main
-
-[options.package_data]
-bee_migration = data/*.csv, data/*.json, templates/*.html, static/*
-
-# Flake8 configuration
-[flake8]
-max-line-length = 88
-max-complexity = 10
-select = E,W,F,C
-ignore =
- # Line too long (handled by black)
- E501,
- # Line break before binary operator (conflicts with black)
- W503,
- # Line break after binary operator (conflicts with black)
- W504,
- # Whitespace before ':' (conflicts with black)
- E203,
- # Multiple imports on one line
- E401,
- # Module level import not at top of file
- E402,
-exclude =
- .git,
- __pycache__,
- .pytest_cache,
- .mypy_cache,
- .tox,
- .venv,
- venv,
- env,
- build,
- dist,
- migrations,
- docs,
- notebooks,
- examples,
- scripts,
- data,
- images,
- results,
- htmlcov,
- .coverage,
- *.egg-info
-per-file-ignores =
- # Tests can use assert statements
- tests/*:S101
- # __init__.py files can have unused imports
- __init__.py:F401
- # Setup files can have imports not at top
- setup.py:E402
- # Configuration files
- conftest.py:E402
-count = True
-statistics = True
-show-source = True
-benchmark = True
-
-# pydocstyle configuration
-[pydocstyle]
-inherit = false
-ignore = D100,D101,D102,D103,D104,D105,D106,D107,D200,D203,D212,D213,D400,D401,D402,D415
-match = .*\.py
-match-dir = (?!tests|migrations|docs|build|dist|htmlcov|\.)[^\.].*
-convention = google
-add-ignore = D100,D101,D102,D103,D104,D105,D106,D107
-
-# mypy configuration (additional to pyproject.toml)
-[mypy]
-python_version = 3.8
-warn_return_any = True
-warn_unused_configs = True
-disallow_untyped_defs = False
-disallow_incomplete_defs = False
-check_untyped_defs = True
-disallow_untyped_decorators = False
-no_implicit_optional = True
-warn_redundant_casts = True
-warn_unused_ignores = True
-warn_no_return = True
-warn_unreachable = True
-strict_equality = True
-show_error_codes = True
-ignore_missing_imports = True
-
-[mypy-geopandas.*]
-ignore_missing_imports = True
-
-[mypy-folium.*]
-ignore_missing_imports = True
-
-[mypy-plotly.*]
-ignore_missing_imports = True
-
-[mypy-seaborn.*]
-ignore_missing_imports = True
-
-[mypy-sklearn.*]
-ignore_missing_imports = True
-
-[mypy-scipy.*]
-ignore_missing_imports = True
-
-[mypy-cartopy.*]
-ignore_missing_imports = True
-
-[mypy-shapely.*]
-ignore_missing_imports = True
-
-[mypy-fiona.*]
-ignore_missing_imports = True
-
-[mypy-pyproj.*]
-ignore_missing_imports = True
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 74d5b03..0000000
--- a/setup.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-"""
-Setup script for Bee Migration Analysis Project.
-
-This script allows the project to be installed as a Python package,
-making it easier to import modules and run commands.
-"""
-
-import os
-import sys
-from pathlib import Path
-
-from setuptools import find_packages, setup
-
-# Ensure we're in the right directory
-here = Path(__file__).parent.absolute()
-
-# Read the README file
-readme_file = here / "README.md"
-if readme_file.exists():
- with open(readme_file, "r", encoding="utf-8") as f:
- long_description = f.read()
-else:
- long_description = "Bee Migration Analysis: Climate Impact Assessment"
-
-# Read requirements
-requirements_file = here / "requirements.txt"
-if requirements_file.exists():
- with open(requirements_file, "r", encoding="utf-8") as f:
- requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")]
-else:
- requirements = [
- "pandas>=1.3.0",
- "numpy>=1.21.0",
- "matplotlib>=3.4.0",
- "seaborn>=0.11.0",
- "scikit-learn>=1.0.0",
- "geopandas>=0.10.0",
- "folium>=0.12.0",
- "plotly>=5.0.0",
- "requests>=2.25.0",
- ]
-
-# Read development requirements
-dev_requirements_file = here / "requirements-dev.txt"
-if dev_requirements_file.exists():
- with open(dev_requirements_file, "r", encoding="utf-8") as f:
- dev_requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")]
-else:
- dev_requirements = [
- "pytest>=7.0.0",
- "pytest-cov>=4.0.0",
- "black>=22.0.0",
- "flake8>=5.0.0",
- "mypy>=0.991",
- "isort>=5.10.0",
- ]
-
-# Version
-version = "1.0.0"
-
-# Package metadata
-setup(
- name="bee-migration-analysis",
- version=version,
- author="Diego Gomes",
- author_email="diego@example.com",
- description="Climate Impact Assessment on Bee Migration Patterns",
- long_description=long_description,
- long_description_content_type="text/markdown",
- url="https://github.com/diegogo/bee-migration-analysis",
- project_urls={
- "Bug Reports": "https://github.com/diegogo/bee-migration-analysis/issues",
- "Source": "https://github.com/diegogo/bee-migration-analysis",
- "Documentation": "https://github.com/diegogo/bee-migration-analysis/blob/main/README.md",
- },
- packages=find_packages(where="src"),
- package_dir={"": "src"},
- classifiers=[
- "Development Status :: 4 - Beta",
- "Intended Audience :: Science/Research",
- "Topic :: Scientific/Engineering :: Bio-Informatics",
- "Topic :: Scientific/Engineering :: Atmospheric Science",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Operating System :: OS Independent",
- "Environment :: Console",
- "Natural Language :: English",
- ],
- keywords="bees, migration, climate, analysis, machine-learning, geospatial",
- python_requires=">=3.8",
- install_requires=requirements,
- extras_require={
- "dev": dev_requirements,
- "test": [
- "pytest>=7.0.0",
- "pytest-cov>=4.0.0",
- "pytest-mock>=3.8.0",
- "coverage>=6.0.0",
- ],
- "docs": [
- "sphinx>=4.0.0",
- "sphinx-rtd-theme>=1.0.0",
- "myst-parser>=0.18.0",
- ],
- "viz": [
- "plotly>=5.0.0",
- "dash>=2.0.0",
- "streamlit>=1.0.0",
- ],
- "api": [
- "fastapi>=0.70.0",
- "uvicorn>=0.15.0",
- "pydantic>=1.8.0",
- ],
- },
- entry_points={
- "console_scripts": [
- "bee-analysis=bee_migration.cli:main",
- "generate-charts=bee_migration.charts:main",
- "run-analysis=bee_migration.analysis:main",
- ],
- },
- include_package_data=True,
- package_data={
- "bee_migration": [
- "data/*.csv",
- "data/*.json",
- "templates/*.html",
- "static/*",
- ],
- },
- zip_safe=False,
- platforms=["any"],
- license="MIT",
- test_suite="tests",
- tests_require=dev_requirements,
-)
-
-# Post-installation message
-if "install" in sys.argv:
- print("\n" + "="*60)
- print("🐝 Bee Migration Analysis Project Installed Successfully! 🐝")
- print("="*60)
- print("\nNext steps:")
- print("1. Run 'bee-analysis --help' to see available commands")
- print("2. Generate charts with 'generate-charts'")
- print("3. Check the notebooks/ directory for tutorials")
- print("4. Read the documentation in README.md")
- print("\nFor development:")
- print("- Install dev dependencies: pip install -e .[dev]")
- print("- Run tests: pytest")
- print("- Format code: black src/ tests/")
- print("\nHappy analyzing! 🔬📊")
- print("="*60 + "\n")
\ No newline at end of file
diff --git a/src/__pycache__/bee_analysis.cpython-312.pyc b/src/__pycache__/bee_analysis.cpython-312.pyc
deleted file mode 100644
index 67d2d6c..0000000
Binary files a/src/__pycache__/bee_analysis.cpython-312.pyc and /dev/null differ
diff --git a/src/__pycache__/config.cpython-312.pyc b/src/__pycache__/config.cpython-312.pyc
deleted file mode 100644
index c3b7c48..0000000
Binary files a/src/__pycache__/config.cpython-312.pyc and /dev/null differ
diff --git a/src/bee_analysis.py b/src/bee_analysis.py
deleted file mode 100644
index f5cac49..0000000
--- a/src/bee_analysis.py
+++ /dev/null
@@ -1,677 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Análise Principal do Comportamento de Abelhas e Mudanças Climáticas.
-
-Este módulo implementa a análise exploratória completa dos dados de abelhas
-e variáveis climáticas na América do Sul, seguindo metodologia científica rigorosa.
-
-Autor: Diego Gomes
-Data: 2024
-Projeto: Bee Climate Analysis
-"""
-
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import seaborn as sns
-from datetime import datetime, timedelta
-import logging
-import warnings
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional
-
-# Imports locais
-from config import (
- DATA_CONFIG, ANALYSIS_CONFIG, VISUALIZATION_CONFIG,
- LOGGING_CONFIG, RESULTS_DIR, IMAGES_DIR, PROCESSED_DATA_DIR
-)
-
-# Configurar warnings e logging
-warnings.filterwarnings('ignore')
-logging.basicConfig(**LOGGING_CONFIG)
-logger = logging.getLogger(__name__)
-
-class BeeClimateAnalyzer:
- """
- Classe principal para análise do impacto das mudanças climáticas
- no comportamento de abelhas na América do Sul.
- """
-
- def __init__(self):
- """
- Inicializa o analisador com configurações padrão.
- """
- self.data = None
- self.climate_data = None
- self.bee_data = None
- self.results = {}
-
- # Configurar estilo de visualização
- plt.style.use(VISUALIZATION_CONFIG['PLOT_STYLE'])
- sns.set_palette(VISUALIZATION_CONFIG['COLOR_PALETTE'])
- plt.rcParams['figure.figsize'] = VISUALIZATION_CONFIG['FIGURE_SIZE']
- plt.rcParams['savefig.dpi'] = VISUALIZATION_CONFIG['DPI']
-
- logger.info("BeeClimateAnalyzer inicializado")
-
- def load_data(self, data_path: Optional[str] = None) -> pd.DataFrame:
- """
- Carrega dados de abelhas e clima.
-
- Args:
- data_path (str, optional): Caminho para arquivo de dados personalizado
-
- Returns:
- pd.DataFrame: Dados carregados e processados
- """
- logger.info("Iniciando carregamento de dados...")
-
- if data_path and Path(data_path).exists():
- self.data = pd.read_csv(data_path)
- logger.info(f"Dados carregados de: {data_path}")
- else:
- # Gerar dados simulados para demonstração
- self.data = self._generate_sample_data()
- logger.info("Dados simulados gerados para demonstração")
-
- # Processar e limpar dados
- self.data = self._preprocess_data(self.data)
-
- # Separar dados climáticos e de abelhas
- self._separate_data_types()
-
- logger.info(f"Dados carregados: {len(self.data)} registros")
- return self.data
-
- def _generate_sample_data(self) -> pd.DataFrame:
- """
- Gera dados simulados realistas para demonstração.
-
- Returns:
- pd.DataFrame: Dados simulados
- """
- np.random.seed(42)
-
- # Configurações temporais
- start_year = DATA_CONFIG['SAMPLING']['START_YEAR']
- end_year = DATA_CONFIG['SAMPLING']['END_YEAR']
- years = np.arange(start_year, end_year + 1)
- n_years = len(years)
-
- # Gerar dados para múltiplas regiões
- regions = ['Norte', 'Nordeste', 'Centro-Oeste', 'Sudeste', 'Sul']
- data_list = []
-
- for region in regions:
- # Dados climáticos com tendências realistas
- base_temp = {'Norte': 26.5, 'Nordeste': 25.8, 'Centro-Oeste': 24.2,
- 'Sudeste': 22.1, 'Sul': 19.8}[region]
-
- temp_trend = np.linspace(0, 2.8, n_years) # Aquecimento de 2.8°C
- temperature = base_temp + temp_trend + np.random.normal(0, 0.6, n_years)
-
- # Precipitação com variabilidade
- base_precip = {'Norte': 2200, 'Nordeste': 800, 'Centro-Oeste': 1400,
- 'Sudeste': 1300, 'Sul': 1600}[region]
-
- precip_trend = np.linspace(0, -150, n_years) # Redução de chuvas
- precipitation = base_precip + precip_trend + np.random.normal(0, 100, n_years)
- precipitation = np.maximum(precipitation, 300) # Mínimo de 300mm
-
- # Dados de abelhas
- base_abundance = {'Norte': 85, 'Nordeste': 65, 'Centro-Oeste': 75,
- 'Sudeste': 70, 'Sul': 60}[region]
-
- abundance_decline = np.linspace(0, -20, n_years) # Declínio de abundância
- bee_abundance = base_abundance + abundance_decline + np.random.normal(0, 5, n_years)
- bee_abundance = np.maximum(bee_abundance, 20) # Mínimo de 20
-
- # Diversidade de espécies
- base_diversity = {'Norte': 45, 'Nordeste': 35, 'Centro-Oeste': 40,
- 'Sudeste': 38, 'Sul': 32}[region]
-
- diversity_decline = np.linspace(0, -8, n_years)
- species_diversity = base_diversity + diversity_decline + np.random.normal(0, 2, n_years)
- species_diversity = np.maximum(species_diversity, 15)
-
- # Produção de mel
- base_honey = {'Norte': 28, 'Nordeste': 22, 'Centro-Oeste': 26,
- 'Sudeste': 24, 'Sul': 30}[region]
-
- honey_decline = np.linspace(0, -6, n_years)
- honey_production = base_honey + honey_decline + np.random.normal(0, 2, n_years)
- honey_production = np.maximum(honey_production, 12)
-
- # Coordenadas geográficas aproximadas
- region_coords = {
- 'Norte': (-3.0, -60.0),
- 'Nordeste': (-9.0, -40.0),
- 'Centro-Oeste': (-15.0, -56.0),
- 'Sudeste': (-20.0, -44.0),
- 'Sul': (-27.0, -51.0)
- }
-
- lat, lon = region_coords[region]
-
- # Criar DataFrame para a região
- region_data = pd.DataFrame({
- 'year': years,
- 'region': region,
- 'latitude': lat + np.random.normal(0, 1, n_years),
- 'longitude': lon + np.random.normal(0, 2, n_years),
- 'temperature': temperature,
- 'precipitation': precipitation,
- 'bee_abundance': bee_abundance,
- 'species_diversity': species_diversity,
- 'honey_production': honey_production
- })
-
- data_list.append(region_data)
-
- return pd.concat(data_list, ignore_index=True)
-
- def _preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame:
- """
- Preprocessa e limpa os dados.
-
- Args:
- data (pd.DataFrame): Dados brutos
-
- Returns:
- pd.DataFrame: Dados processados
- """
- logger.info("Iniciando preprocessamento dos dados...")
-
- # Converter year para datetime
- data['date'] = pd.to_datetime(data['year'], format='%Y')
-
- # Criar variáveis derivadas
- data['temperature_anomaly'] = data.groupby('region')['temperature'].transform(
- lambda x: x - x.mean()
- )
-
- data['precipitation_anomaly'] = data.groupby('region')['precipitation'].transform(
- lambda x: x - x.mean()
- )
-
- # Calcular índices compostos
- data['climate_stress_index'] = (
- data['temperature_anomaly'].abs() +
- data['precipitation_anomaly'].abs()
- ) / 2
-
- data['bee_health_index'] = (
- data['bee_abundance'] * 0.4 +
- data['species_diversity'] * 0.3 +
- data['honey_production'] * 0.3
- )
-
- # Remover outliers extremos
- numeric_columns = data.select_dtypes(include=[np.number]).columns
- for col in numeric_columns:
- Q1 = data[col].quantile(0.01)
- Q3 = data[col].quantile(0.99)
- data[col] = data[col].clip(lower=Q1, upper=Q3)
-
- logger.info("Preprocessamento concluído")
- return data
-
- def _separate_data_types(self):
- """
- Separa dados climáticos e de abelhas para análises específicas.
- """
- climate_cols = ['year', 'region', 'latitude', 'longitude', 'date',
- 'temperature', 'precipitation', 'temperature_anomaly',
- 'precipitation_anomaly', 'climate_stress_index']
-
- bee_cols = ['year', 'region', 'latitude', 'longitude', 'date',
- 'bee_abundance', 'species_diversity', 'honey_production',
- 'bee_health_index']
-
- self.climate_data = self.data[climate_cols].copy()
- self.bee_data = self.data[bee_cols].copy()
-
- def analyze_temporal_patterns(self) -> Dict:
- """
- Analisa padrões temporais nos dados.
-
- Returns:
- Dict: Resultados da análise temporal
- """
- logger.info("Iniciando análise de padrões temporais...")
-
- results = {}
-
- # Análise de tendências por região
- trends = {}
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region]
-
- # Calcular tendências lineares
- temp_trend = np.polyfit(region_data['year'], region_data['temperature'], 1)[0]
- precip_trend = np.polyfit(region_data['year'], region_data['precipitation'], 1)[0]
- bee_trend = np.polyfit(region_data['year'], region_data['bee_abundance'], 1)[0]
-
- trends[region] = {
- 'temperature_trend': temp_trend,
- 'precipitation_trend': precip_trend,
- 'bee_abundance_trend': bee_trend
- }
-
- results['regional_trends'] = trends
-
- # Análise de sazonalidade (simulada)
- seasonal_patterns = self._analyze_seasonal_patterns()
- results['seasonal_patterns'] = seasonal_patterns
-
- # Detecção de pontos de mudança
- change_points = self._detect_change_points()
- results['change_points'] = change_points
-
- self.results['temporal_analysis'] = results
- logger.info("Análise temporal concluída")
-
- return results
-
- def _analyze_seasonal_patterns(self) -> Dict:
- """
- Analisa padrões sazonais (simulado para dados anuais).
-
- Returns:
- Dict: Padrões sazonais identificados
- """
- # Para dados anuais, simular variação sazonal
- seasonal_data = {}
-
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region]
-
- # Simular variação sazonal baseada na latitude
- lat = region_data['latitude'].mean()
-
- # Regiões mais ao norte têm menor variação sazonal
- seasonal_amplitude = max(0.5, abs(lat) * 0.1)
-
- seasonal_data[region] = {
- 'temperature_seasonality': seasonal_amplitude,
- 'precipitation_seasonality': seasonal_amplitude * 1.5,
- 'bee_activity_seasonality': seasonal_amplitude * 0.8
- }
-
- return seasonal_data
-
- def _detect_change_points(self) -> Dict:
- """
- Detecta pontos de mudança significativa nas séries temporais.
-
- Returns:
- Dict: Pontos de mudança detectados
- """
- change_points = {}
-
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region].sort_values('year')
-
- # Detectar mudanças usando diferenças de segunda ordem
- temp_diff = np.diff(region_data['temperature'], n=2)
- bee_diff = np.diff(region_data['bee_abundance'], n=2)
-
- # Identificar anos com mudanças significativas
- temp_threshold = np.std(temp_diff) * 2
- bee_threshold = np.std(bee_diff) * 2
-
- temp_changes = region_data['year'].iloc[2:][np.abs(temp_diff) > temp_threshold].tolist()
- bee_changes = region_data['year'].iloc[2:][np.abs(bee_diff) > bee_threshold].tolist()
-
- change_points[region] = {
- 'temperature_changes': temp_changes,
- 'bee_abundance_changes': bee_changes
- }
-
- return change_points
-
- def analyze_correlations(self) -> Dict:
- """
- Analisa correlações entre variáveis climáticas e de abelhas.
-
- Returns:
- Dict: Resultados da análise de correlação
- """
- logger.info("Iniciando análise de correlações...")
-
- results = {}
-
- # Variáveis para análise de correlação
- correlation_vars = [
- 'temperature', 'precipitation', 'temperature_anomaly', 'precipitation_anomaly',
- 'climate_stress_index', 'bee_abundance', 'species_diversity',
- 'honey_production', 'bee_health_index'
- ]
-
- # Matriz de correlação geral
- correlation_matrix = self.data[correlation_vars].corr()
- results['overall_correlation'] = correlation_matrix
-
- # Correlações por região
- regional_correlations = {}
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region]
- regional_corr = region_data[correlation_vars].corr()
- regional_correlations[region] = regional_corr
-
- results['regional_correlations'] = regional_correlations
-
- # Análise de lag correlations
- lag_correlations = self._analyze_lag_correlations()
- results['lag_correlations'] = lag_correlations
-
- # Correlações significativas
- significant_correlations = self._identify_significant_correlations(correlation_matrix)
- results['significant_correlations'] = significant_correlations
-
- self.results['correlation_analysis'] = results
- logger.info("Análise de correlações concluída")
-
- return results
-
- def _analyze_lag_correlations(self) -> Dict:
- """
- Analisa correlações com defasagem temporal.
-
- Returns:
- Dict: Correlações com lag
- """
- lag_results = {}
- lags = ANALYSIS_CONFIG['CORRELATION']['LAG_ANALYSIS_PERIODS']
-
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region].sort_values('year')
-
- lag_correlations = {}
- for lag in lags:
- if lag == 0:
- corr = region_data['temperature'].corr(region_data['bee_abundance'])
- else:
- if len(region_data) > lag:
- temp_lagged = region_data['temperature'].iloc[:-lag]
- bee_current = region_data['bee_abundance'].iloc[lag:]
- corr = temp_lagged.corr(bee_current)
- else:
- corr = np.nan
-
- lag_correlations[f'lag_{lag}'] = corr
-
- lag_results[region] = lag_correlations
-
- return lag_results
-
- def _identify_significant_correlations(self, correlation_matrix: pd.DataFrame) -> List[Dict]:
- """
- Identifica correlações estatisticamente significativas.
-
- Args:
- correlation_matrix (pd.DataFrame): Matriz de correlação
-
- Returns:
- List[Dict]: Lista de correlações significativas
- """
- threshold = ANALYSIS_CONFIG['CORRELATION']['MIN_CORRELATION_THRESHOLD']
- significant = []
-
- for i, var1 in enumerate(correlation_matrix.columns):
- for j, var2 in enumerate(correlation_matrix.columns):
- if i < j: # Evitar duplicatas
- corr_value = correlation_matrix.loc[var1, var2]
- if abs(corr_value) >= threshold:
- significant.append({
- 'variable_1': var1,
- 'variable_2': var2,
- 'correlation': corr_value,
- 'strength': 'strong' if abs(corr_value) >= 0.7 else 'moderate'
- })
-
- return sorted(significant, key=lambda x: abs(x['correlation']), reverse=True)
-
- def analyze_geographic_patterns(self) -> Dict:
- """
- Analisa padrões geográficos e espaciais.
-
- Returns:
- Dict: Resultados da análise geográfica
- """
- logger.info("Iniciando análise de padrões geográficos...")
-
- results = {}
-
- # Análise por região
- regional_summary = self.data.groupby('region').agg({
- 'temperature': ['mean', 'std', 'min', 'max'],
- 'precipitation': ['mean', 'std', 'min', 'max'],
- 'bee_abundance': ['mean', 'std', 'min', 'max'],
- 'species_diversity': ['mean', 'std', 'min', 'max'],
- 'honey_production': ['mean', 'std', 'min', 'max']
- }).round(2)
-
- results['regional_summary'] = regional_summary
-
- # Gradientes latitudinais
- latitudinal_gradients = self._analyze_latitudinal_gradients()
- results['latitudinal_gradients'] = latitudinal_gradients
-
- # Hotspots de mudança
- change_hotspots = self._identify_change_hotspots()
- results['change_hotspots'] = change_hotspots
-
- self.results['geographic_analysis'] = results
- logger.info("Análise geográfica concluída")
-
- return results
-
- def _analyze_latitudinal_gradients(self) -> Dict:
- """
- Analisa gradientes latitudinais nas variáveis.
-
- Returns:
- Dict: Gradientes latitudinais
- """
- # Calcular correlações com latitude
- lat_correlations = {}
- variables = ['temperature', 'precipitation', 'bee_abundance',
- 'species_diversity', 'honey_production']
-
- for var in variables:
- corr = self.data['latitude'].corr(self.data[var])
- lat_correlations[var] = corr
-
- return lat_correlations
-
- def _identify_change_hotspots(self) -> Dict:
- """
- Identifica regiões com maiores mudanças.
-
- Returns:
- Dict: Hotspots de mudança
- """
- hotspots = {}
-
- for region in self.data['region'].unique():
- region_data = self.data[self.data['region'] == region]
-
- # Calcular magnitude de mudança
- temp_change = region_data['temperature'].max() - region_data['temperature'].min()
- bee_change = region_data['bee_abundance'].max() - region_data['bee_abundance'].min()
-
- hotspots[region] = {
- 'temperature_change': temp_change,
- 'bee_abundance_change': bee_change,
- 'total_change_score': temp_change + bee_change
- }
-
- return hotspots
-
- def generate_summary_report(self) -> str:
- """
- Gera relatório resumo da análise.
-
- Returns:
- str: Relatório em formato texto
- """
- logger.info("Gerando relatório resumo...")
-
- report = []
- report.append("=" * 80)
- report.append("RELATÓRIO DE ANÁLISE: ABELHAS E MUDANÇAS CLIMÁTICAS")
- report.append("=" * 80)
- report.append(f"Data da análise: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
- report.append(f"Período analisado: {self.data['year'].min()} - {self.data['year'].max()}")
- report.append(f"Regiões analisadas: {', '.join(self.data['region'].unique())}")
- report.append(f"Total de registros: {len(self.data)}")
- report.append("")
-
- # Resumo dos dados
- report.append("RESUMO DOS DADOS")
- report.append("-" * 40)
- report.append(f"Temperatura média: {self.data['temperature'].mean():.1f}°C")
- report.append(f"Precipitação média: {self.data['precipitation'].mean():.0f}mm")
- report.append(f"Abundância média de abelhas: {self.data['bee_abundance'].mean():.1f}")
- report.append(f"Diversidade média de espécies: {self.data['species_diversity'].mean():.1f}")
- report.append(f"Produção média de mel: {self.data['honey_production'].mean():.1f}kg")
- report.append("")
-
- # Tendências principais
- if 'temporal_analysis' in self.results:
- report.append("TENDÊNCIAS PRINCIPAIS")
- report.append("-" * 40)
-
- trends = self.results['temporal_analysis']['regional_trends']
- for region, trend_data in trends.items():
- temp_trend = trend_data['temperature_trend']
- bee_trend = trend_data['bee_abundance_trend']
-
- report.append(f"{region}:")
- report.append(f" - Temperatura: {temp_trend:+.3f}°C/ano")
- report.append(f" - Abundância de abelhas: {bee_trend:+.2f}/ano")
-
- report.append("")
-
- # Correlações significativas
- if 'correlation_analysis' in self.results:
- report.append("CORRELAÇÕES SIGNIFICATIVAS")
- report.append("-" * 40)
-
- sig_corr = self.results['correlation_analysis']['significant_correlations']
- for corr in sig_corr[:5]: # Top 5
- report.append(f"{corr['variable_1']} ↔ {corr['variable_2']}: {corr['correlation']:.3f} ({corr['strength']})")
-
- report.append("")
-
- # Conclusões
- report.append("CONCLUSÕES PRINCIPAIS")
- report.append("-" * 40)
- report.append("1. Observa-se tendência de aquecimento em todas as regiões")
- report.append("2. Declínio geral na abundância de abelhas correlacionado com temperatura")
- report.append("3. Variações regionais significativas nos padrões de mudança")
- report.append("4. Necessidade de monitoramento contínuo e medidas de conservação")
- report.append("")
-
- report.append("=" * 80)
-
- report_text = "\n".join(report)
-
- # Salvar relatório
- report_path = RESULTS_DIR / "analysis_summary_report.txt"
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write(report_text)
-
- logger.info(f"Relatório salvo em: {report_path}")
- return report_text
-
- def save_results(self):
- """
- Salva todos os resultados da análise.
- """
- logger.info("Salvando resultados da análise...")
-
- # Salvar dados processados
- self.data.to_csv(PROCESSED_DATA_DIR / "bee_climate_processed.csv", index=False)
-
- # Salvar resultados específicos
- for analysis_type, results in self.results.items():
- if isinstance(results, dict):
- # Converter para DataFrame quando possível
- if analysis_type == 'correlation_analysis' and 'overall_correlation' in results:
- results['overall_correlation'].to_csv(
- RESULTS_DIR / f"{analysis_type}_correlation_matrix.csv"
- )
-
- logger.info("Resultados salvos com sucesso")
-
- def run_complete_analysis(self, data_path: Optional[str] = None) -> Dict:
- """
- Executa análise completa.
-
- Args:
- data_path (str, optional): Caminho para dados personalizados
-
- Returns:
- Dict: Todos os resultados da análise
- """
- logger.info("Iniciando análise completa...")
-
- # Carregar dados
- self.load_data(data_path)
-
- # Executar todas as análises
- temporal_results = self.analyze_temporal_patterns()
- correlation_results = self.analyze_correlations()
- geographic_results = self.analyze_geographic_patterns()
-
- # Gerar relatório
- summary_report = self.generate_summary_report()
-
- # Salvar resultados
- self.save_results()
-
- logger.info("Análise completa finalizada")
-
- return {
- 'temporal_analysis': temporal_results,
- 'correlation_analysis': correlation_results,
- 'geographic_analysis': geographic_results,
- 'summary_report': summary_report,
- 'data_summary': {
- 'total_records': len(self.data),
- 'regions': list(self.data['region'].unique()),
- 'year_range': (self.data['year'].min(), self.data['year'].max())
- }
- }
-
-def main():
- """
- Função principal para executar a análise.
- """
- print("Iniciando Análise de Abelhas e Mudanças Climáticas")
- print("=" * 60)
-
- # Criar analisador
- analyzer = BeeClimateAnalyzer()
-
- # Executar análise completa
- results = analyzer.run_complete_analysis()
-
- # Exibir resumo
- print("\nRESUMO DA ANÁLISE")
- print("-" * 30)
- print(f"Total de registros analisados: {results['data_summary']['total_records']}")
- print(f"Regiões: {', '.join(results['data_summary']['regions'])}")
- print(f"Período: {results['data_summary']['year_range'][0]} - {results['data_summary']['year_range'][1]}")
-
- print("\nAnálise concluída com sucesso!")
- print(f"Resultados salvos em: {RESULTS_DIR}")
- print(f"Gráficos disponíveis em: {IMAGES_DIR}")
-
- return results
-
-if __name__ == "__main__":
- results = main()
\ No newline at end of file
diff --git a/src/bee_migration/__init__.py b/src/bee_migration/__init__.py
new file mode 100644
index 0000000..bf76304
--- /dev/null
+++ b/src/bee_migration/__init__.py
@@ -0,0 +1 @@
+"""Climate impact assessment on bee migration patterns in South America."""
diff --git a/src/bee_migration/__main__.py b/src/bee_migration/__main__.py
new file mode 100644
index 0000000..95da4c5
--- /dev/null
+++ b/src/bee_migration/__main__.py
@@ -0,0 +1,5 @@
+import sys
+
+from bee_migration.cli import main
+
+sys.exit(main())
diff --git a/src/bee_migration/analysis/__init__.py b/src/bee_migration/analysis/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/analysis/correlation.py b/src/bee_migration/analysis/correlation.py
new file mode 100644
index 0000000..3d7cb52
--- /dev/null
+++ b/src/bee_migration/analysis/correlation.py
@@ -0,0 +1,90 @@
+from collections.abc import Sequence
+from dataclasses import dataclass
+from itertools import combinations
+
+import pandas as pd
+
+from bee_migration.climate.observations import MEASUREMENTS, Observations
+
+CORRELATED_COLUMNS = [
+ *MEASUREMENTS,
+ "temperature_anomaly",
+ "precipitation_anomaly",
+ "climate_stress_index",
+ "bee_health_index",
+]
+MODERATE_THRESHOLD = 0.3
+NOTABLE_THRESHOLD = MODERATE_THRESHOLD
+STRONG_THRESHOLD = 0.7
+YEAR_LAGS = (0, 1, 2, 3)
+
+
+@dataclass(frozen=True)
+class CorrelationPair:
+ variables: tuple[str, str]
+ coefficient: float
+
+ def magnitude(self) -> float:
+ return abs(self.coefficient)
+
+ def strength(self) -> str:
+ if self.magnitude() >= STRONG_THRESHOLD:
+ return "strong"
+ if self.magnitude() >= MODERATE_THRESHOLD:
+ return "moderate"
+ return "weak"
+
+ def record(self) -> dict[str, str | float]:
+ first, second = self.variables
+ return {
+ "first": first,
+ "second": second,
+ "coefficient": self.coefficient,
+ "strength": self.strength(),
+ }
+
+ def describe(self) -> str:
+ first, second = self.variables
+ return f"{first} <-> {second}: {self.coefficient:+.3f} ({self.strength()})"
+
+
+class Correlations:
+ """Linear relationships between climate and bee measurements."""
+
+ def __init__(self, observations: Observations) -> None:
+ self._observations = observations
+
+ def matrix(self) -> pd.DataFrame:
+ return self._observations.table()[CORRELATED_COLUMNS].corr()
+
+ def by_region(self) -> dict[str, pd.DataFrame]:
+ return {
+ region: rows[CORRELATED_COLUMNS].corr()
+ for region, rows in self._observations.by_region()
+ }
+
+ def notable_pairs(
+ self, threshold: float = NOTABLE_THRESHOLD
+ ) -> list[CorrelationPair]:
+ """Distinct variable pairs correlated at least `threshold`, strongest first."""
+ matrix = self.matrix()
+ pairs = [
+ CorrelationPair((first, second), float(matrix.loc[first, second]))
+ for first, second in combinations(CORRELATED_COLUMNS, 2)
+ ]
+ notable = [pair for pair in pairs if pair.magnitude() >= threshold]
+ return sorted(notable, key=CorrelationPair.magnitude, reverse=True)
+
+ def lagged(self, lags: Sequence[int] = YEAR_LAGS) -> pd.DataFrame:
+ """Temperature against bee abundance `lag` years later, by region."""
+ delayed = {
+ region: _delayed_response(rows, lags)
+ for region, rows in self._observations.by_region()
+ }
+ return pd.DataFrame.from_dict(delayed, orient="index")
+
+
+def _delayed_response(rows: pd.DataFrame, lags: Sequence[int]) -> dict[int, float]:
+ temperature = rows["temperature"]
+ abundance = rows["bee_abundance"]
+ return {lag: float(temperature.corr(abundance.shift(-lag))) for lag in lags}
diff --git a/src/bee_migration/analysis/geographic.py b/src/bee_migration/analysis/geographic.py
new file mode 100644
index 0000000..b071e0a
--- /dev/null
+++ b/src/bee_migration/analysis/geographic.py
@@ -0,0 +1,43 @@
+import pandas as pd
+
+from bee_migration.climate.observations import MEASUREMENTS, Observations
+
+SUMMARY_STATISTICS = ["mean", "std", "min", "max"]
+
+
+class GeographicPatterns:
+ """How measurements differ between regions and along latitude."""
+
+ def __init__(self, observations: Observations) -> None:
+ self._observations = observations
+
+ def summary(self) -> pd.DataFrame:
+ measurements = self._observations.table().groupby("region")[list(MEASUREMENTS)]
+ return measurements.agg(SUMMARY_STATISTICS).round(2)
+
+ def latitudinal_gradients(self) -> pd.Series:
+ """Correlation of every measurement with latitude."""
+ table = self._observations.table()
+ return table[list(MEASUREMENTS)].corrwith(table["latitude"])
+
+ def hotspots(self) -> pd.DataFrame:
+ """Regions ranked by how much temperature and bee abundance swung."""
+ swings = {
+ region: _swings(rows) for region, rows in self._observations.by_region()
+ }
+ ranked = pd.DataFrame.from_dict(swings, orient="index")
+ return ranked.sort_values("total_change_score", ascending=False)
+
+
+def _range(series: pd.Series) -> float:
+ return float(series.max() - series.min())
+
+
+def _swings(rows: pd.DataFrame) -> dict[str, float]:
+ temperature = _range(rows["temperature"])
+ abundance = _range(rows["bee_abundance"])
+ return {
+ "temperature_change": temperature,
+ "bee_abundance_change": abundance,
+ "total_change_score": temperature + abundance,
+ }
diff --git a/src/bee_migration/analysis/temporal.py b/src/bee_migration/analysis/temporal.py
new file mode 100644
index 0000000..a7886c6
--- /dev/null
+++ b/src/bee_migration/analysis/temporal.py
@@ -0,0 +1,47 @@
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.observations import MEASUREMENTS, Observations
+
+CHANGE_POINT_COLUMNS = ("temperature", "bee_abundance")
+CHANGE_POINT_SIGMAS = 2.0
+
+
+class TemporalPatterns:
+ """How measurements evolve over the years within each region."""
+
+ def __init__(self, observations: Observations) -> None:
+ self._observations = observations
+
+ def trends(self) -> pd.DataFrame:
+ """Linear change per year of every measurement, by region."""
+ slopes = {
+ region: _yearly_slopes(rows)
+ for region, rows in self._observations.by_region()
+ }
+ return pd.DataFrame.from_dict(slopes, orient="index")
+
+ def change_points(self) -> pd.DataFrame:
+ """Years where a series bends unusually sharply, by region."""
+ changes = {
+ region: _change_years(rows)
+ for region, rows in self._observations.by_region()
+ }
+ return pd.DataFrame.from_dict(changes, orient="index")
+
+
+def _yearly_slopes(rows: pd.DataFrame) -> dict[str, float]:
+ return {
+ name: float(np.polyfit(rows["year"], rows[name], 1)[0]) for name in MEASUREMENTS
+ }
+
+
+def _change_years(rows: pd.DataFrame) -> dict[str, list[int]]:
+ return {name: _sharp_bends(rows, name) for name in CHANGE_POINT_COLUMNS}
+
+
+def _sharp_bends(rows: pd.DataFrame, column: str) -> list[int]:
+ curvature = np.diff(rows[column].to_numpy(), n=2)
+ threshold = CHANGE_POINT_SIGMAS * curvature.std()
+ later_years = rows["year"].to_numpy()[2:]
+ return later_years[np.abs(curvature) > threshold].tolist()
diff --git a/src/bee_migration/charts/__init__.py b/src/bee_migration/charts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/charts/evidence.py b/src/bee_migration/charts/evidence.py
new file mode 100644
index 0000000..b3a404a
--- /dev/null
+++ b/src/bee_migration/charts/evidence.py
@@ -0,0 +1,149 @@
+"""Charts of the figures gathered from the cited studies."""
+
+import pandas as pd
+from matplotlib.axes import Axes
+from matplotlib.figure import Figure
+
+from bee_migration.charts.panels import (
+ Labels,
+ bar_panel,
+ grouped_bar_panel,
+ horizontal_bar_panel,
+ lines_panel,
+)
+from bee_migration.evidence import literature
+
+
+def _titled(title: str) -> tuple[Figure, list[Axes]]:
+ figure = Figure(figsize=(16, 12))
+ figure.suptitle(title, fontsize=16, fontweight="bold")
+ return figure, list(figure.subplots(2, 2).flat)
+
+
+def ufpr_migration_analysis() -> Figure:
+ figure, (species, directions, factors, intensity) = _titled(
+ "UFPR Migration Analysis: South American Bees Moving South"
+ )
+ grouped_bar_panel(
+ species,
+ literature.SPECIES_BY_REGION,
+ ["lightblue", "darkblue"],
+ Labels("Species Distribution by Region", "Brazilian Regions", "Species"),
+ )
+ bar_panel(
+ directions,
+ literature.MIGRATION_DIRECTIONS,
+ ["darkgreen", "green", "lightgreen", "gray", "red"],
+ Labels("Migration Direction Distribution", "Direction", "Species (%)"),
+ unit="%",
+ )
+ horizontal_bar_panel(
+ factors,
+ literature.IMPACT_FACTORS,
+ "orange",
+ Labels("Migration Impact Factors", "Impact Intensity (1-10)"),
+ )
+ _intensity_panel(intensity)
+ return figure
+
+
+def comparative_temperature_analysis() -> Figure:
+ figure, (warming, timeline, thresholds, biomes) = _titled(
+ "Temperature Trends Comparison: Local Data vs Scientific Studies"
+ )
+ bar_panel(
+ warming,
+ literature.TEMPERATURE_INCREASE,
+ ["red", "orange", "yellow", "blue", "purple"],
+ Labels("Projected Temperature Increase by Region", "", "Increase (°C)"),
+ unit="°C",
+ )
+ _timeline_panel(timeline)
+ _threshold_panel(thresholds)
+ horizontal_bar_panel(
+ biomes,
+ literature.BIOME_VULNERABILITY,
+ ["darkgreen", "brown", "green", "yellow", "lightgreen"],
+ Labels("Climate Vulnerability Index by Biome", "Vulnerability Index (1-10)"),
+ )
+ return figure
+
+
+def migration_patterns_synthesis() -> Figure:
+ figure, (studies, scenarios, groups, priorities) = _titled(
+ "Bee Migration Patterns: Scientific Evidence Synthesis"
+ )
+ bar_panel(
+ studies,
+ literature.SOUTHWARD_SHARE_BY_STUDY,
+ ["green", "blue", "orange", "purple"],
+ Labels("Species with Southward Migration", "", "Southward Migration (%)"),
+ unit="%",
+ )
+ _scenario_panel(scenarios)
+ grouped_bar_panel(
+ groups,
+ literature.FUNCTIONAL_RESPONSE,
+ ["red", "blue"],
+ Labels("Functional Group Response to Climate Change", "", "Percentage (%)"),
+ )
+ _priority_panel(priorities)
+ return figure
+
+
+def _intensity_panel(axis: Axes) -> None:
+ intensity = pd.Series(literature.MIGRATION_INTENSITY)
+ axis.fill_between(intensity.index, intensity.to_numpy(), alpha=0.3, color="blue")
+ axis.plot(intensity.index, intensity.to_numpy(), "bo-", linewidth=3)
+ labels = Labels(
+ "Southward Migration Intensity Projection", "Year", "Migrating Species (%)"
+ )
+ labels.apply(axis)
+
+
+def _timeline_panel(axis: Axes) -> None:
+ table = pd.DataFrame(literature.ABUNDANCE_TIMELINE, index=literature.TIMELINE_YEARS)
+ labels = Labels("Species Abundance Timeline", "Year", "Relative Abundance (%)")
+ lines_panel(axis, table, ["red", "blue"], labels)
+ axis.axvline(x=literature.PRESENT_YEAR, color="black", linestyle="--", alpha=0.7)
+
+
+def _threshold_panel(axis: Axes) -> None:
+ bar_panel(
+ axis,
+ literature.CRITICAL_WARMING,
+ ["green", "blue", "orange", "red"],
+ Labels("Critical Warming by Functional Group", "", "Critical Increase (°C)"),
+ unit="°C",
+ )
+ axis.axhline(
+ y=literature.AVERAGE_CRITICAL_WARMING,
+ color="red",
+ linestyle="--",
+ label="Global Average Threshold",
+ )
+ axis.legend()
+
+
+def _scenario_panel(axis: Axes) -> None:
+ table = pd.DataFrame(
+ literature.MIGRATION_SCENARIOS, index=literature.SCENARIO_YEARS
+ )
+ lowest, highest = table.min(axis=1), table.max(axis=1)
+ axis.fill_between(table.index, lowest, highest, alpha=0.2, color="gray")
+ labels = Labels(
+ "Migration Velocity Projections", "Year", "Cumulative Migration (%)"
+ )
+ lines_panel(axis, table, ["green", "blue", "red"], labels)
+
+
+def _priority_panel(axis: Axes) -> None:
+ richness = axis.twinx()
+ areas = list(literature.PRIORITY_SCORES)
+ axis.bar(
+ areas, list(literature.PRIORITY_SCORES.values()), color="darkgreen", alpha=0.7
+ )
+ richness.plot(areas, list(literature.SPECIES_RICHNESS.values()), "ro-", linewidth=2)
+ richness.set_ylabel("Species Richness", color="red")
+ axis.tick_params(axis="x", labelsize=8)
+ Labels("Conservation Priority Areas", "", "Priority Score (1-10)").apply(axis)
diff --git a/src/bee_migration/charts/gallery.py b/src/bee_migration/charts/gallery.py
new file mode 100644
index 0000000..8c8cbe9
--- /dev/null
+++ b/src/bee_migration/charts/gallery.py
@@ -0,0 +1,48 @@
+from pathlib import Path
+
+import pandas as pd
+from matplotlib.figure import Figure
+
+from bee_migration.charts import evidence, observed
+
+PUBLICATION_RESOLUTION = 300
+
+
+class ChartGallery:
+ """Renders every project chart as an image file in one directory."""
+
+ def __init__(
+ self, directory: Path, resolution: int = PUBLICATION_RESOLUTION
+ ) -> None:
+ self._directory = directory
+ self._resolution = resolution
+
+ def render(self, series: pd.DataFrame, distribution: pd.DataFrame) -> list[Path]:
+ """Draw all charts from the continental series and the range points."""
+ self._directory.mkdir(parents=True, exist_ok=True)
+ figures = {**_observed(series.set_index("year"), distribution), **_evidence()}
+ return [self._saved(name, figure) for name, figure in figures.items()]
+
+ def _saved(self, name: str, figure: Figure) -> Path:
+ path = self._directory / f"{name}.png"
+ figure.tight_layout()
+ figure.savefig(path, dpi=self._resolution, bbox_inches="tight")
+ return path
+
+
+def _observed(series: pd.DataFrame, distribution: pd.DataFrame) -> dict[str, Figure]:
+ return {
+ "temperature_migration_trend": observed.temperature_migration_trend(series),
+ "correlation_analysis": observed.correlation_analysis(series),
+ "species_honey_decline": observed.species_honey_decline(series),
+ "geographic_distribution": observed.geographic_distribution(distribution),
+ "climate_impact_summary": observed.climate_impact_summary(series),
+ }
+
+
+def _evidence() -> dict[str, Figure]:
+ return {
+ "ufpr_migration_analysis": evidence.ufpr_migration_analysis(),
+ "comparative_temperature_analysis": evidence.comparative_temperature_analysis(),
+ "migration_patterns_synthesis": evidence.migration_patterns_synthesis(),
+ }
diff --git a/src/bee_migration/charts/observed.py b/src/bee_migration/charts/observed.py
new file mode 100644
index 0000000..9221745
--- /dev/null
+++ b/src/bee_migration/charts/observed.py
@@ -0,0 +1,187 @@
+"""Charts of the continental series and of the projected range shift."""
+
+import numpy as np
+import pandas as pd
+import seaborn as sns
+from matplotlib.axes import Axes
+from matplotlib.figure import Figure
+
+from bee_migration.charts.panels import (
+ Labels,
+ area_trend_panel,
+ lines_panel,
+ scatter_panel,
+ trend_panel,
+)
+
+PROJECTION_HORIZON = 2030
+IMPACTED = ["temperature", "species_count", "honey_production"]
+IMPACT_NAMES = {
+ "temperature": "Temperature",
+ "latitude": "Southward Migration",
+ "species_count": "Diversity",
+ "honey_production": "Honey Production",
+}
+IMPACT_COLORS = {
+ "Temperature": "red",
+ "Southward Migration": "blue",
+ "Diversity": "green",
+ "Honey Production": "orange",
+}
+SCATTERS = (
+ ("latitude", "purple", Labels("Temperature vs Bee Latitude", "", "Latitude (°)")),
+ (
+ "species_count",
+ "green",
+ Labels("Temperature vs Species Diversity", "", "Number of Species"),
+ ),
+ (
+ "honey_production",
+ "orange",
+ Labels("Temperature vs Honey Production", "", "Honey Production (kg/hive)"),
+ ),
+)
+RANGE_COLORMAPS = {"Current (2020-2023)": "Reds", "Projected (2040-2050)": "Blues"}
+
+
+def temperature_migration_trend(series: pd.DataFrame) -> Figure:
+ figure = Figure(figsize=(14, 10))
+ warming, migration = figure.subplots(2, 1)
+ trend_panel(
+ warming,
+ series["temperature"],
+ "red",
+ Labels(
+ "Annual Average Temperature Increase in South America",
+ "Year",
+ "Temperature (°C)",
+ ),
+ )
+ trend_panel(
+ migration,
+ series["latitude"],
+ "blue",
+ Labels("Southward Bee Migration (Average Latitude)", "Year", "Latitude (°)"),
+ )
+ return figure
+
+
+def correlation_analysis(series: pd.DataFrame) -> Figure:
+ figure = Figure(figsize=(16, 12))
+ matrix, *scatters = figure.subplots(2, 2).flat
+ sns.heatmap(
+ series.corr(), annot=True, cmap="RdYlBu_r", center=0, square=True, ax=matrix
+ )
+ matrix.set_title("Correlation Matrix Between Variables", fontweight="bold")
+ for axis, (column, color, labels) in zip(scatters, SCATTERS, strict=True):
+ described = Labels(labels.title, "Temperature (°C)", labels.vertical)
+ scatter_panel(axis, series["temperature"], series[column], color, described)
+ return figure
+
+
+def species_honey_decline(series: pd.DataFrame) -> Figure:
+ figure = Figure(figsize=(16, 6))
+ species, honey = figure.subplots(1, 2)
+ area_trend_panel(
+ species,
+ series["species_count"],
+ "darkred",
+ Labels(
+ "Decline in Bee Species Diversity", "Year", "Number of Observed Species"
+ ),
+ )
+ area_trend_panel(
+ honey,
+ series["honey_production"],
+ "darkorange",
+ Labels("Impact on Honey Production", "Year", "Average Production (kg/hive)"),
+ )
+ return figure
+
+
+def geographic_distribution(distribution: pd.DataFrame) -> Figure:
+ figure = Figure(figsize=(18, 8))
+ axes = figure.subplots(1, 2)
+ for axis, period in zip(axes, RANGE_COLORMAPS, strict=True):
+ _range_panel(axis, distribution[distribution["period"] == period], period)
+ return figure
+
+
+def climate_impact_summary(series: pd.DataFrame) -> Figure:
+ figure = Figure(figsize=(16, 12))
+ normalized, spread, change, projection = figure.subplots(2, 2).flat
+ _normalized_panel(normalized, series)
+ _spread_panel(spread, series)
+ _change_panel(change, series)
+ _projection_panel(projection, series[IMPACTED])
+ return figure
+
+
+def _range_panel(axis: Axes, points: pd.DataFrame, period: str) -> None:
+ dots = axis.scatter(
+ points["longitude"],
+ points["latitude"],
+ c=points["abundance"],
+ s=60,
+ alpha=0.7,
+ cmap=RANGE_COLORMAPS[period],
+ edgecolors="black",
+ linewidth=0.5,
+ )
+ axis.figure.colorbar(dots, ax=axis, label="Relative Abundance")
+ title = period.replace(" (", " Bee Distribution\n(")
+ Labels(title, "Longitude", "Latitude").apply(axis)
+
+
+def _named(table: pd.DataFrame) -> pd.DataFrame:
+ return table.rename(columns=IMPACT_NAMES)
+
+
+def _colors(table: pd.DataFrame) -> list[str]:
+ return [IMPACT_COLORS[name] for name in table.columns]
+
+
+def _scaled(table: pd.DataFrame) -> pd.DataFrame:
+ """Rescale every column to run from 0 to 100."""
+ return (table - table.min()) / (table.max() - table.min()) * 100
+
+
+def _normalized_panel(axis: Axes, series: pd.DataFrame) -> None:
+ southward = series.assign(latitude=-series["latitude"])
+ table = _named(_scaled(southward))
+ labels = Labels("Normalized Trends (0-100)", "Year", "Normalized Index")
+ lines_panel(axis, table, _colors(table), labels)
+
+
+def _spread_panel(axis: Axes, series: pd.DataFrame) -> None:
+ magnitudes = _named(series.assign(latitude=series["latitude"].abs()))
+ magnitudes.plot.box(ax=axis)
+ Labels("Variable Distribution").apply(axis)
+
+
+def _change_panel(axis: Axes, series: pd.DataFrame) -> None:
+ first_year = series.index.min()
+ table = _named((series[IMPACTED] / series[IMPACTED].iloc[0] - 1) * 100)
+ axis.axhline(y=0, color="black", linestyle="--", alpha=0.5)
+ labels = Labels(f"Percentage Change Since {first_year}", "Year", "Change (%)")
+ lines_panel(axis, table, _colors(table), labels)
+
+
+def _extrapolated(series: pd.DataFrame) -> pd.DataFrame:
+ """Continue every column's linear trend up to the projection horizon."""
+ future = np.arange(series.index.max() + 1, PROJECTION_HORIZON + 1)
+ columns = {
+ name: np.polyval(np.polyfit(series.index, values, 1), future)
+ for name, values in series.items()
+ }
+ return pd.DataFrame(columns, index=future)
+
+
+def _projection_panel(axis: Axes, series: pd.DataFrame) -> None:
+ last_year = series.index.max()
+ table = _named(_scaled(pd.concat([series, _extrapolated(series)])))
+ colors = _colors(table)
+ labels = Labels(f"Projection to {PROJECTION_HORIZON}", "Year", "Normalized Index")
+ table.loc[last_year:].plot(ax=axis, style="--", color=colors, legend=False)
+ lines_panel(axis, table.loc[:last_year], colors, labels)
+ axis.axvline(x=last_year, color="black", linestyle=":", alpha=0.7)
diff --git a/src/bee_migration/charts/panels.py b/src/bee_migration/charts/panels.py
new file mode 100644
index 0000000..bb5ad90
--- /dev/null
+++ b/src/bee_migration/charts/panels.py
@@ -0,0 +1,88 @@
+"""Reusable chart panels drawn onto a single matplotlib axis."""
+
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+
+import numpy as np
+import pandas as pd
+from matplotlib.axes import Axes
+
+GRID_OPACITY = 0.3
+FILL_OPACITY = 0.7
+Values = Mapping[str, float]
+
+
+@dataclass(frozen=True)
+class Labels:
+ title: str
+ horizontal: str = ""
+ vertical: str = ""
+
+ def apply(self, axis: Axes) -> None:
+ axis.set_title(self.title, fontsize=14, fontweight="bold")
+ axis.set_xlabel(self.horizontal)
+ axis.set_ylabel(self.vertical)
+ axis.grid(True, alpha=GRID_OPACITY)
+
+
+def fitted_line(horizontal: Sequence[float], vertical: Sequence[float]) -> np.ndarray:
+ return np.polyval(np.polyfit(horizontal, vertical, 1), horizontal)
+
+
+def trend_panel(axis: Axes, series: pd.Series, color: str, labels: Labels) -> None:
+ """A yearly series with its linear trend."""
+ axis.plot(series.index, series.to_numpy(), "o-", color=color, linewidth=2)
+ fit = fitted_line(series.index, series.to_numpy())
+ axis.plot(series.index, fit, "--", color="black", alpha=0.8, linewidth=2)
+ labels.apply(axis)
+
+
+def area_trend_panel(axis: Axes, series: pd.Series, color: str, labels: Labels) -> None:
+ axis.fill_between(series.index, series.to_numpy(), alpha=GRID_OPACITY, color=color)
+ trend_panel(axis, series, color, labels)
+
+
+def scatter_panel(
+ axis: Axes, horizontal: pd.Series, vertical: pd.Series, color: str, labels: Labels
+) -> None:
+ """Two variables against each other with the fitted regression line."""
+ axis.scatter(horizontal, vertical, alpha=FILL_OPACITY, s=60, color=color)
+ axis.plot(horizontal, fitted_line(horizontal, vertical), "r--", alpha=0.8)
+ labels.apply(axis)
+
+
+def bar_panel(
+ axis: Axes, values: Values, colors: Sequence[str], labels: Labels, unit: str = ""
+) -> None:
+ bars = axis.bar(
+ list(values), list(values.values()), color=colors, alpha=FILL_OPACITY
+ )
+ axis.bar_label(bars, fmt=lambda value: f"{value:g}{unit}", padding=2)
+ labels.apply(axis)
+
+
+def horizontal_bar_panel(
+ axis: Axes, values: Values, colors: Sequence[str] | str, labels: Labels
+) -> None:
+ bars = axis.barh(
+ list(values), list(values.values()), color=colors, alpha=FILL_OPACITY
+ )
+ axis.bar_label(bars, fmt="%g", padding=2)
+ labels.apply(axis)
+
+
+def grouped_bar_panel(
+ axis: Axes, groups: Mapping[str, Values], colors: Sequence[str], labels: Labels
+) -> None:
+ """Side-by-side bars, one colour per group."""
+ table = pd.DataFrame(groups)
+ table.plot.bar(ax=axis, color=list(colors), alpha=FILL_OPACITY, rot=0, width=0.7)
+ labels.apply(axis)
+
+
+def lines_panel(
+ axis: Axes, table: pd.DataFrame, colors: Sequence[str], labels: Labels
+) -> None:
+ """One line per column, sharing the table's index as the horizontal axis."""
+ table.plot(ax=axis, style="o-", color=list(colors), linewidth=2)
+ labels.apply(axis)
diff --git a/src/bee_migration/cli.py b/src/bee_migration/cli.py
new file mode 100644
index 0000000..2916633
--- /dev/null
+++ b/src/bee_migration/cli.py
@@ -0,0 +1,107 @@
+"""Command line entry point: analyse, predict and chart bee migration."""
+
+import argparse
+import logging
+from collections.abc import Sequence
+from pathlib import Path
+
+from bee_migration.analysis.correlation import Correlations
+from bee_migration.charts.gallery import ChartGallery
+from bee_migration.climate.continental import continental_series, distribution_shift
+from bee_migration.climate.observations import Observations
+from bee_migration.climate.period import DEFAULT_PERIOD
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.climate.simulation import DEFAULT_SEED, ClimateSimulation
+from bee_migration.prediction.catalog import DEFAULT_CANDIDATES
+from bee_migration.prediction.training import MigrationPredictor
+from bee_migration.reports.analysis_report import AnalysisReport
+from bee_migration.reports.prediction_report import PredictionReport
+from bee_migration.workspace import Workspace
+
+logger = logging.getLogger(__name__)
+
+
+def main(arguments: Sequence[str] | None = None) -> int:
+ options = _parser().parse_args(arguments)
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
+ try:
+ options.command(options)
+ except (OSError, ValueError) as error:
+ logger.error("Could not complete '%s': %s", options.name, error)
+ return 1
+ return 0
+
+
+def _parser() -> argparse.ArgumentParser:
+ shared = argparse.ArgumentParser(add_help=False)
+ shared.add_argument("--root", type=Path, default=Path.cwd(), help="project folder")
+ shared.add_argument(
+ "--data", type=Path, help="observations CSV; simulated if absent"
+ )
+ shared.add_argument("--seed", type=int, default=DEFAULT_SEED)
+ parser = argparse.ArgumentParser(prog="bee-migration", description=__doc__)
+ commands = parser.add_subparsers(dest="name", required=True)
+ for name, command in COMMANDS.items():
+ commands.add_parser(name, parents=[shared], help=command.__doc__).set_defaults(
+ command=command
+ )
+ return parser
+
+
+def _observations(options: argparse.Namespace) -> Observations:
+ if options.data is not None:
+ return Observations.from_csv(options.data)
+ simulation = ClimateSimulation(BRAZILIAN_REGIONS, DEFAULT_PERIOD)
+ return simulation.observations(options.seed)
+
+
+def analyze(options: argparse.Namespace) -> None:
+ """Summarise trends and correlations in the observations."""
+ workspace = Workspace(options.root)
+ observations = _observations(options)
+ observations.save(workspace.data("processed/bee_climate_processed.csv"))
+ matrix = Correlations(observations).matrix()
+ matrix.to_csv(workspace.result("correlation_analysis_correlation_matrix.csv"))
+ report = workspace.result("analysis_summary_report.txt")
+ AnalysisReport(observations).save(report)
+ logger.info("Analysis of %d records written to %s", len(observations), report)
+
+
+def predict(options: argparse.Namespace) -> None:
+ """Train models forecasting bee abundance and compare them."""
+ workspace = Workspace(options.root)
+ leaderboard = MigrationPredictor(DEFAULT_CANDIDATES).train(_observations(options))
+ leaderboard.save(workspace.result("models"))
+ leaderboard.metrics().to_csv(workspace.result("model_metrics.csv"))
+ leaderboard.importances().to_csv(workspace.result("feature_importance.csv"))
+ report = workspace.result("ml_predictions_report.txt")
+ PredictionReport(leaderboard).save(report)
+ logger.info("Comparison of %d models written to %s", len(leaderboard), report)
+
+
+def charts(options: argparse.Namespace) -> None:
+ """Render every chart shown in the README."""
+ workspace = Workspace(options.root)
+ if options.data is not None:
+ logger.warning("Charts draw the simulated continental series, not --data")
+ series = continental_series(DEFAULT_PERIOD, options.seed)
+ distribution = distribution_shift(options.seed)
+ rendered = ChartGallery(workspace.images()).render(series, distribution)
+ series.to_csv(workspace.data("bee_climate_data.csv"), index=False)
+ distribution.to_csv(workspace.data("bee_geographic_data.csv"), index=False)
+ logger.info("%d charts written to %s", len(rendered), workspace.images())
+
+
+def everything(options: argparse.Namespace) -> None:
+ """Run the analysis, the predictions and the charts."""
+ analyze(options)
+ predict(options)
+ charts(options)
+
+
+COMMANDS = {
+ "analyze": analyze,
+ "predict": predict,
+ "charts": charts,
+ "all": everything,
+}
diff --git a/src/bee_migration/climate/__init__.py b/src/bee_migration/climate/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/climate/continental.py b/src/bee_migration/climate/continental.py
new file mode 100644
index 0000000..f4829c8
--- /dev/null
+++ b/src/bee_migration/climate/continental.py
@@ -0,0 +1,56 @@
+"""Synthetic continent-wide series and range maps used by the charts."""
+
+from dataclasses import dataclass
+
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import Coordinates
+from bee_migration.climate.simulation import DEFAULT_SEED
+from bee_migration.climate.trend import Drift, Trend
+
+SERIES_TRENDS = {
+ "temperature": Trend(Drift(22.5, 2.5), spread=0.5),
+ "latitude": Trend(Drift(-15.0, -3.0), spread=0.3),
+ "species_count": Trend(Drift(45.0, -8.0), spread=2.0, floor=20.0),
+ "honey_production": Trend(Drift(25.0, -5.0), spread=1.5, floor=15.0),
+}
+RANGE_SPREAD = Coordinates(latitude=4.0, longitude=8.0)
+
+
+@dataclass(frozen=True)
+class SpeciesRange:
+ """Where bees are found during a period, and how abundant they are."""
+
+ period: str
+ centre: Coordinates
+ mean_abundance: float
+
+ def points(self, count: int, generator: np.random.Generator) -> pd.DataFrame:
+ positions = self.centre.scattered(RANGE_SPREAD, count, generator)
+ abundance = generator.exponential(self.mean_abundance, count)
+ return positions.assign(period=self.period, abundance=abundance)
+
+
+RANGES = (
+ SpeciesRange("Current (2020-2023)", Coordinates(-12.0, -55.0), 2.0),
+ SpeciesRange("Projected (2040-2050)", Coordinates(-18.0, -58.0), 1.5),
+)
+
+
+def continental_series(period: StudyPeriod, seed: int = DEFAULT_SEED) -> pd.DataFrame:
+ generator = np.random.default_rng(seed)
+ columns = {
+ name: trend.sample(period.length(), generator)
+ for name, trend in SERIES_TRENDS.items()
+ }
+ return pd.DataFrame({"year": period.years(), **columns})
+
+
+def distribution_shift(
+ seed: int = DEFAULT_SEED, points_per_period: int = 100
+) -> pd.DataFrame:
+ generator = np.random.default_rng(seed)
+ clouds = [species.points(points_per_period, generator) for species in RANGES]
+ return pd.concat(clouds, ignore_index=True)
diff --git a/src/bee_migration/climate/observations.py b/src/bee_migration/climate/observations.py
new file mode 100644
index 0000000..71313a5
--- /dev/null
+++ b/src/bee_migration/climate/observations.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+from pathlib import Path
+
+import pandas as pd
+
+CLIMATE_MEASUREMENTS = ("temperature", "precipitation")
+BEE_MEASUREMENTS = ("bee_abundance", "species_diversity", "honey_production")
+MEASUREMENTS = (*CLIMATE_MEASUREMENTS, *BEE_MEASUREMENTS)
+NUMERIC_COLUMNS = ("year", "latitude", "longitude", *MEASUREMENTS)
+REQUIRED_COLUMNS = ("year", "region", "latitude", "longitude", *MEASUREMENTS)
+HEALTH_WEIGHTS = {
+ "bee_abundance": 0.4,
+ "species_diversity": 0.3,
+ "honey_production": 0.3,
+}
+
+
+class Observations:
+ """Yearly climate and bee measurements per region, with derived indices."""
+
+ def __init__(self, table: pd.DataFrame) -> None:
+ missing = [name for name in REQUIRED_COLUMNS if name not in table.columns]
+ if missing:
+ raise ValueError(f"Observations are missing columns: {', '.join(missing)}")
+ if table.empty:
+ raise ValueError("Observations cannot be empty")
+ self._table = _with_indices(_consistent(_ordered(_numeric(table))))
+
+ @classmethod
+ def from_csv(cls, path: Path) -> Observations:
+ return cls(pd.read_csv(path))
+
+ def __len__(self) -> int:
+ return len(self._table)
+
+ def table(self) -> pd.DataFrame:
+ return self._table.copy()
+
+ def region_names(self) -> list[str]:
+ return sorted(self._table["region"].unique())
+
+ def by_region(self) -> Iterator[tuple[str, pd.DataFrame]]:
+ for region, rows in self._table.groupby("region"):
+ yield str(region), rows.copy()
+
+ def save(self, path: Path) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ self._table.to_csv(path, index=False)
+
+
+def _numeric(table: pd.DataFrame) -> pd.DataFrame:
+ try:
+ numbers = {name: pd.to_numeric(table[name]) for name in NUMERIC_COLUMNS}
+ except (TypeError, ValueError) as error:
+ raise ValueError(f"Observations hold non-numeric values: {error}") from error
+ return table.assign(**numbers)
+
+
+def _skips_years(years: pd.Series) -> bool:
+ return years.max() - years.min() + 1 != len(years)
+
+
+def _consistent(table: pd.DataFrame) -> pd.DataFrame:
+ """Reject tables the yearly lags and trends cannot be computed from."""
+ if table.isna().any().any():
+ raise ValueError("Observations contain missing values")
+ if table.duplicated(["region", "year"]).any():
+ raise ValueError("Observations repeat a year within a region")
+ if table.groupby("region")["year"].agg(_skips_years).any():
+ raise ValueError("Observations skip years within a region")
+ return table
+
+
+def _ordered(table: pd.DataFrame) -> pd.DataFrame:
+ raw = table[list(REQUIRED_COLUMNS)]
+ return raw.sort_values(["region", "year"]).reset_index(drop=True)
+
+
+def _anomaly(table: pd.DataFrame, column: str) -> pd.Series:
+ return table[column] - table.groupby("region")[column].transform("mean")
+
+
+def _standardised(table: pd.DataFrame, column: str) -> pd.Series:
+ return _anomaly(table, column) / table.groupby("region")[column].transform("std")
+
+
+def _stress(table: pd.DataFrame) -> pd.Series:
+ """Mean absolute departure from the regional norm, in standard deviations."""
+ temperature = _standardised(table, "temperature").abs()
+ precipitation = _standardised(table, "precipitation").abs()
+ return (temperature + precipitation) / 2
+
+
+def _health(table: pd.DataFrame) -> pd.Series:
+ weighted = [table[name] * weight for name, weight in HEALTH_WEIGHTS.items()]
+ return sum(weighted[1:], start=weighted[0])
+
+
+def _with_indices(table: pd.DataFrame) -> pd.DataFrame:
+ return table.assign(
+ temperature_anomaly=_anomaly(table, "temperature"),
+ precipitation_anomaly=_anomaly(table, "precipitation"),
+ climate_stress_index=_stress(table),
+ bee_health_index=_health(table),
+ )
diff --git a/src/bee_migration/climate/period.py b/src/bee_migration/climate/period.py
new file mode 100644
index 0000000..0aaf979
--- /dev/null
+++ b/src/bee_migration/climate/period.py
@@ -0,0 +1,26 @@
+from dataclasses import dataclass
+
+import numpy as np
+
+
+@dataclass(frozen=True)
+class StudyPeriod:
+ """Inclusive range of years covered by a study."""
+
+ first_year: int
+ last_year: int
+
+ def __post_init__(self) -> None:
+ if self.last_year < self.first_year:
+ raise ValueError(
+ f"Last year {self.last_year} is before first year {self.first_year}"
+ )
+
+ def years(self) -> np.ndarray:
+ return np.arange(self.first_year, self.last_year + 1)
+
+ def length(self) -> int:
+ return self.last_year - self.first_year + 1
+
+
+DEFAULT_PERIOD = StudyPeriod(2000, 2023)
diff --git a/src/bee_migration/climate/region.py b/src/bee_migration/climate/region.py
new file mode 100644
index 0000000..4fd3bff
--- /dev/null
+++ b/src/bee_migration/climate/region.py
@@ -0,0 +1,81 @@
+from collections.abc import Mapping
+from dataclasses import dataclass
+
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.observations import MEASUREMENTS
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.trend import Drift, Trend
+
+
+@dataclass(frozen=True)
+class Coordinates:
+ latitude: float
+ longitude: float
+
+ def scattered(
+ self, spread: "Coordinates", count: int, generator: np.random.Generator
+ ) -> pd.DataFrame:
+ """Random positions around this point, `spread` being the deviations."""
+ return pd.DataFrame(
+ {
+ "latitude": generator.normal(self.latitude, spread.latitude, count),
+ "longitude": generator.normal(self.longitude, spread.longitude, count),
+ }
+ )
+
+
+SAMPLING_SPREAD = Coordinates(latitude=1.0, longitude=2.0)
+
+
+@dataclass(frozen=True)
+class RegionProfile:
+ """How a region's climate and bee populations evolve over a study."""
+
+ name: str
+ centre: Coordinates
+ trends: Mapping[str, Trend]
+
+ def simulate(
+ self, period: StudyPeriod, generator: np.random.Generator
+ ) -> pd.DataFrame:
+ length = period.length()
+ positions = self.centre.scattered(SAMPLING_SPREAD, length, generator)
+ measurements = {
+ name: trend.sample(length, generator) for name, trend in self.trends.items()
+ }
+ return positions.assign(year=period.years(), region=self.name, **measurements)
+
+
+# Change over the whole study, shared by every region: warming, drying, decline.
+STUDY_WIDE_CHANGE = {
+ "temperature": Trend(Drift(0.0, 2.8), spread=0.6),
+ "precipitation": Trend(Drift(0.0, -150.0), spread=100.0, floor=300.0),
+ "bee_abundance": Trend(Drift(0.0, -20.0), spread=5.0, floor=20.0),
+ "species_diversity": Trend(Drift(0.0, -8.0), spread=2.0, floor=15.0),
+ "honey_production": Trend(Drift(0.0, -6.0), spread=2.0, floor=12.0),
+}
+
+
+def _profile(
+ name: str, centre: Coordinates, baselines: tuple[float, ...]
+) -> RegionProfile:
+ starts = dict(zip(MEASUREMENTS, baselines, strict=True))
+ trends = {
+ name: STUDY_WIDE_CHANGE[name].starting_at(start)
+ for name, start in starts.items()
+ }
+ return RegionProfile(name, centre, trends)
+
+
+# Baselines follow MEASUREMENTS: °C, mm/year, abundance index, species, kg/hive.
+BRAZILIAN_REGIONS = (
+ _profile("North", Coordinates(-3.0, -60.0), (26.5, 2200.0, 85.0, 45.0, 28.0)),
+ _profile("Northeast", Coordinates(-9.0, -40.0), (25.8, 800.0, 65.0, 35.0, 22.0)),
+ _profile(
+ "Center-West", Coordinates(-15.0, -56.0), (24.2, 1400.0, 75.0, 40.0, 26.0)
+ ),
+ _profile("Southeast", Coordinates(-20.0, -44.0), (22.1, 1300.0, 70.0, 38.0, 24.0)),
+ _profile("South", Coordinates(-27.0, -51.0), (19.8, 1600.0, 60.0, 32.0, 30.0)),
+)
diff --git a/src/bee_migration/climate/simulation.py b/src/bee_migration/climate/simulation.py
new file mode 100644
index 0000000..241a08d
--- /dev/null
+++ b/src/bee_migration/climate/simulation.py
@@ -0,0 +1,25 @@
+from collections.abc import Sequence
+
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.observations import Observations
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import RegionProfile
+
+DEFAULT_SEED = 42
+
+
+class ClimateSimulation:
+ """Generates synthetic observations for demonstrating the analysis."""
+
+ def __init__(self, profiles: Sequence[RegionProfile], period: StudyPeriod) -> None:
+ self._profiles = tuple(profiles)
+ self._period = period
+
+ def observations(self, seed: int = DEFAULT_SEED) -> Observations:
+ generator = np.random.default_rng(seed)
+ tables = [
+ profile.simulate(self._period, generator) for profile in self._profiles
+ ]
+ return Observations(pd.concat(tables, ignore_index=True))
diff --git a/src/bee_migration/climate/trend.py b/src/bee_migration/climate/trend.py
new file mode 100644
index 0000000..043103a
--- /dev/null
+++ b/src/bee_migration/climate/trend.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+from dataclasses import dataclass, replace
+
+import numpy as np
+
+
+@dataclass(frozen=True)
+class Drift:
+ """Steady change of a quantity from a starting value."""
+
+ start: float
+ total_change: float
+
+ def path(self, length: int) -> np.ndarray:
+ return np.linspace(self.start, self.start + self.total_change, length)
+
+ def starting_at(self, start: float) -> Drift:
+ return replace(self, start=start)
+
+
+@dataclass(frozen=True)
+class Trend:
+ """A drift observed with random year-to-year variation."""
+
+ drift: Drift
+ spread: float
+ floor: float = -np.inf
+
+ def sample(self, length: int, generator: np.random.Generator) -> np.ndarray:
+ noise = generator.normal(0.0, self.spread, length)
+ return np.maximum(self.drift.path(length) + noise, self.floor)
+
+ def starting_at(self, start: float) -> Trend:
+ return replace(self, drift=self.drift.starting_at(start))
diff --git a/src/bee_migration/evidence/__init__.py b/src/bee_migration/evidence/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/evidence/literature.py b/src/bee_migration/evidence/literature.py
new file mode 100644
index 0000000..cf80e0b
--- /dev/null
+++ b/src/bee_migration/evidence/literature.py
@@ -0,0 +1,92 @@
+"""Figures summarising the cited studies on climate-driven bee range shifts.
+
+Values were entered by hand as a reading of the sources listed in the README
+(UFPR, Dryad, Ecology Letters, PMC); they are not extracted from the studies'
+datasets.
+"""
+
+REGIONS = ("North", "Northeast", "Center-West", "Southeast", "South")
+
+# UFPR study of 18 species in south-eastern South America
+SPECIES_BY_REGION = {
+ "Current (2020)": dict(zip(REGIONS, (12, 8, 15, 18, 22), strict=True)),
+ "Projected (2040)": dict(zip(REGIONS, (8, 5, 12, 20, 28), strict=True)),
+}
+MIGRATION_DIRECTIONS = {
+ "South": 45,
+ "Southwest": 40,
+ "Southeast": 10,
+ "No Change": 3,
+ "North": 2,
+}
+IMPACT_FACTORS = {
+ "Temperature": 9,
+ "Precipitation": 7,
+ "Land Use": 8,
+ "Monocultures": 6,
+}
+MIGRATION_INTENSITY = {2024: 0, 2029: 15, 2034: 35, 2039: 60, 2044: 80, 2049: 100}
+
+# Local projections set against published ones
+TEMPERATURE_INCREASE = {
+ "North\n(Local Data)": 3.2,
+ "Center\n(Local Data)": 2.8,
+ "South\n(Local Data)": 2.1,
+ "UFPR\nProjection": 2.5,
+ "Aegean\nProjection": 3.5,
+}
+TIMELINE_YEARS = (1960, 1990, 2000, 2010, 2020, 2024, 2030, 2040, 2050)
+ABUNDANCE_TIMELINE = {
+ "Bombus bellicosus (UFPR)": (100, 80, 60, 40, 20, 0, 0, 0, 0),
+ "General Abundance (Local)": (100, 95, 90, 85, 75, 70, 60, 50, 40),
+}
+PRESENT_YEAR = 2024
+CRITICAL_WARMING = {
+ "Eusocial\nSpecies": 2.0,
+ "Solitary\nSpecies": 2.5,
+ "Generalist\nSpecies": 1.8,
+ "Specialist\nSpecies": 3.0,
+}
+AVERAGE_CRITICAL_WARMING = 2.5
+BIOME_VULNERABILITY = {
+ "Amazon": 7.5,
+ "Cerrado": 8.2,
+ "Atlantic Forest": 9.1,
+ "Caatinga": 6.8,
+ "Pampa": 8.8,
+}
+
+# Synthesis across studies
+SOUTHWARD_SHARE_BY_STUDY = {
+ "UFPR\n(18 spp)": 85,
+ "Dryad\n(Neotropical)": 90,
+ "North America\n(Bombus)": 70,
+ "Aegean\n(Islands)": 60,
+}
+SCENARIO_YEARS = tuple(range(2024, 2051, 3))
+MIGRATION_SCENARIOS = {
+ "Conservative Scenario": (0, 10, 25, 40, 55, 70, 80, 85, 90),
+ "Moderate Scenario": (0, 15, 35, 55, 70, 80, 88, 92, 95),
+ "Accelerated Scenario": (0, 20, 45, 65, 80, 90, 95, 98, 100),
+}
+FUNCTIONAL_GROUPS = (
+ "Eusocial\nBees",
+ "Solitary\nBees",
+ "Generalist\nPollinators",
+ "Specialist\nPollinators",
+)
+FUNCTIONAL_RESPONSE = {
+ "Migration Rate (%)": dict(zip(FUNCTIONAL_GROUPS, (75, 85, 65, 95), strict=True)),
+ "Adaptation Capacity (%)": dict(
+ zip(FUNCTIONAL_GROUPS, (60, 40, 80, 20), strict=True)
+ ),
+}
+PRIORITY_AREAS = (
+ "Southern\nBrazilian\nFields",
+ "Atlantic\nForest\nRemnants",
+ "Cerrado\nTransition",
+ "Coastal\nPlains",
+ "Mountain\nCorridors",
+)
+PRIORITY_SCORES = dict(zip(PRIORITY_AREAS, (9.5, 8.8, 8.2, 7.5, 9.0), strict=True))
+SPECIES_RICHNESS = dict(zip(PRIORITY_AREAS, (45, 38, 42, 28, 35), strict=True))
diff --git a/src/bee_migration/prediction/__init__.py b/src/bee_migration/prediction/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/prediction/catalog.py b/src/bee_migration/prediction/catalog.py
new file mode 100644
index 0000000..c83d75b
--- /dev/null
+++ b/src/bee_migration/prediction/catalog.py
@@ -0,0 +1,84 @@
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass
+from typing import Any
+
+from sklearn.base import RegressorMixin, clone
+from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
+from sklearn.linear_model import LinearRegression
+from sklearn.model_selection import GridSearchCV, TimeSeriesSplit
+from sklearn.pipeline import Pipeline
+from sklearn.preprocessing import StandardScaler
+from sklearn.svm import SVR
+
+from bee_migration.prediction.features import Sample
+
+MODEL_SEED = 42
+VALIDATION_FOLDS = 5
+TUNING_SCORE = "neg_mean_squared_error"
+
+
+@dataclass(frozen=True)
+class Candidate:
+ """A regression model and the hyperparameters worth trying for it."""
+
+ name: str
+ estimator: RegressorMixin
+ grid: Mapping[str, Sequence[Any]]
+
+ def tuned(self, sample: Sample) -> Pipeline:
+ """Fit on `sample`, choosing hyperparameters on later-year folds."""
+ pipeline = Pipeline(
+ [("scale", StandardScaler()), ("model", clone(self.estimator))]
+ )
+ if not self.grid:
+ return pipeline.fit(sample.features, sample.target)
+ search = GridSearchCV(
+ pipeline,
+ {f"model__{name}": list(values) for name, values in self.grid.items()},
+ cv=TimeSeriesSplit(n_splits=VALIDATION_FOLDS),
+ scoring=TUNING_SCORE,
+ n_jobs=-1,
+ )
+ return search.fit(sample.features, sample.target).best_estimator_
+
+
+# Fixed hyperparameters: no search, for places where training must be quick.
+QUICK_CANDIDATES = (
+ Candidate("Linear Regression", LinearRegression(), {}),
+ Candidate(
+ "Random Forest",
+ RandomForestRegressor(n_estimators=100, max_depth=5, random_state=MODEL_SEED),
+ {},
+ ),
+ Candidate(
+ "Gradient Boosting",
+ GradientBoostingRegressor(
+ n_estimators=100, learning_rate=0.05, max_depth=2, random_state=MODEL_SEED
+ ),
+ {},
+ ),
+ Candidate("SVR", SVR(C=10, kernel="linear"), {}),
+)
+
+DEFAULT_CANDIDATES = (
+ Candidate("Linear Regression", LinearRegression(), {}),
+ Candidate(
+ "Random Forest",
+ RandomForestRegressor(random_state=MODEL_SEED),
+ {
+ "n_estimators": [100, 300],
+ "max_depth": [5, None],
+ "min_samples_split": [2, 5],
+ },
+ ),
+ Candidate(
+ "Gradient Boosting",
+ GradientBoostingRegressor(random_state=MODEL_SEED),
+ {
+ "n_estimators": [100, 200],
+ "learning_rate": [0.05, 0.1],
+ "max_depth": [2, 3],
+ },
+ ),
+ Candidate("SVR", SVR(), {"C": [1, 10, 100], "kernel": ["rbf", "linear"]}),
+)
diff --git a/src/bee_migration/prediction/features.py b/src/bee_migration/prediction/features.py
new file mode 100644
index 0000000..23c1538
--- /dev/null
+++ b/src/bee_migration/prediction/features.py
@@ -0,0 +1,122 @@
+from collections.abc import Sequence
+from dataclasses import dataclass
+
+import numpy as np
+import pandas as pd
+from sklearn.base import RegressorMixin
+from sklearn.inspection import permutation_importance
+
+from bee_migration.climate.observations import BEE_MEASUREMENTS, Observations
+from bee_migration.prediction.scores import RegressionScore
+
+DEFAULT_TARGET = "bee_abundance"
+IMPORTANCE_REPEATS = 10
+IMPORTANCE_SEED = 42
+CLIMATE_FEATURES = [
+ "year_normalized",
+ "latitude",
+ "longitude",
+ "temperature",
+ "precipitation",
+ "temperature_precipitation_ratio",
+ "temperature_by_latitude",
+ "precipitation_by_latitude",
+ "temperature_previous_year",
+ "precipitation_previous_year",
+ "temperature_change",
+ "precipitation_change",
+]
+
+
+@dataclass(frozen=True)
+class Sample:
+ """Feature rows paired with the value to predict for each."""
+
+ features: np.ndarray
+ target: np.ndarray
+
+ def score(self, model: RegressorMixin) -> RegressionScore:
+ return RegressionScore.of(self.target, model.predict(self.features))
+
+ def importance(self, model: RegressorMixin, names: Sequence[str]) -> pd.Series:
+ """Loss of accuracy when each feature is shuffled."""
+ shuffled = permutation_importance(
+ model,
+ self.features,
+ self.target,
+ n_repeats=IMPORTANCE_REPEATS,
+ random_state=IMPORTANCE_SEED,
+ )
+ return pd.Series(shuffled.importances_mean, index=list(names))
+
+
+@dataclass(frozen=True)
+class TrainTestSplit:
+ train: Sample
+ test: Sample
+
+
+class FeatureTable:
+ """Predictors known before a year's bee measurements are taken."""
+
+ def __init__(
+ self, observations: Observations, target: str = DEFAULT_TARGET
+ ) -> None:
+ if target not in BEE_MEASUREMENTS:
+ raise ValueError(
+ f"Unknown target '{target}', expected one of {BEE_MEASUREMENTS}"
+ )
+ self._target = target
+ self._table = _engineered(observations.table(), target)
+
+ def names(self) -> list[str]:
+ regions = [name for name in self._table.columns if name.startswith("region_")]
+ return [*CLIMATE_FEATURES, *regions, f"{self._target}_previous_year"]
+
+ def split(self, holdout_years: int) -> TrainTestSplit:
+ """Train on the earlier years, test on the most recent `holdout_years`."""
+ recent = self._recent(holdout_years)
+ return TrainTestSplit(self._sample(~recent), self._sample(recent))
+
+ def holdout(self, holdout_years: int) -> pd.DataFrame:
+ """Year, region and observed target of every held-out row, in test order."""
+ rows = self._table[self._recent(holdout_years)]
+ labels = rows[["year", "region", self._target]]
+ return labels.rename(columns={self._target: "actual"}).reset_index(drop=True)
+
+ def _recent(self, holdout_years: int) -> pd.Series:
+ years = sorted(self._table["year"].unique())
+ if not 0 < holdout_years < len(years):
+ raise ValueError(
+ f"Cannot holdout {holdout_years} of {len(years)} usable years"
+ )
+ return self._table["year"] >= years[-holdout_years]
+
+ def _sample(self, selected: pd.Series) -> Sample:
+ rows = self._table[selected]
+ features = rows[self.names()].to_numpy(dtype=float)
+ return Sample(features, rows[self._target].to_numpy(dtype=float))
+
+
+def _engineered(table: pd.DataFrame, target: str) -> pd.DataFrame:
+ by_region = table.groupby("region")
+ first_year = table["year"].min()
+ elapsed = table["year"].max() - first_year
+ lagged = table.assign(
+ year_normalized=(table["year"] - first_year) / max(elapsed, 1),
+ temperature_precipitation_ratio=table["temperature"]
+ / (table["precipitation"] + 1),
+ temperature_by_latitude=table["temperature"] * table["latitude"],
+ precipitation_by_latitude=table["precipitation"] * table["latitude"],
+ temperature_previous_year=by_region["temperature"].shift(1),
+ precipitation_previous_year=by_region["precipitation"].shift(1),
+ **{f"{target}_previous_year": by_region[target].shift(1)},
+ )
+ changed = lagged.assign(
+ temperature_change=lagged["temperature"] - lagged["temperature_previous_year"],
+ precipitation_change=lagged["precipitation"]
+ - lagged["precipitation_previous_year"],
+ )
+ flags = pd.get_dummies(changed["region"], prefix="region", dtype=float)
+ encoded = pd.concat([changed, flags], axis=1)
+ return encoded.dropna().sort_values("year", kind="stable").reset_index(drop=True)
diff --git a/src/bee_migration/prediction/scores.py b/src/bee_migration/prediction/scores.py
new file mode 100644
index 0000000..8920971
--- /dev/null
+++ b/src/bee_migration/prediction/scores.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass
+
+import numpy as np
+from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
+
+SMALLEST_DENOMINATOR = 1e-8
+
+
+@dataclass(frozen=True)
+class RegressionScore:
+ """How closely predictions match the observed values."""
+
+ rmse: float
+ mae: float
+ r2: float
+ mape: float
+
+ @classmethod
+ def of(cls, actual: np.ndarray, predicted: np.ndarray) -> RegressionScore:
+ scale = np.maximum(np.abs(actual), SMALLEST_DENOMINATOR)
+ return cls(
+ rmse=float(np.sqrt(mean_squared_error(actual, predicted))),
+ mae=float(mean_absolute_error(actual, predicted)),
+ r2=float(r2_score(actual, predicted)),
+ mape=float(np.mean(np.abs(actual - predicted) / scale) * 100),
+ )
+
+ def labelled(self, prefix: str) -> dict[str, float]:
+ return {f"{prefix}_{name}": value for name, value in asdict(self).items()}
+
+ def describe(self, label: str) -> list[str]:
+ return [
+ f" {label} R2: {self.r2:.4f}",
+ f" {label} RMSE: {self.rmse:.4f}",
+ f" {label} MAE: {self.mae:.4f}",
+ f" {label} MAPE: {self.mape:.2f}%",
+ ]
+
+
+@dataclass(frozen=True)
+class Evaluation:
+ """Scores on the years a model learned from and on the years held out."""
+
+ train: RegressionScore
+ test: RegressionScore
+
+ def labelled(self) -> dict[str, float]:
+ return {**self.train.labelled("train"), **self.test.labelled("test")}
diff --git a/src/bee_migration/prediction/training.py b/src/bee_migration/prediction/training.py
new file mode 100644
index 0000000..d15048f
--- /dev/null
+++ b/src/bee_migration/prediction/training.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+from collections.abc import Iterator, Sequence
+from dataclasses import dataclass
+from pathlib import Path
+
+import joblib
+import numpy as np
+import pandas as pd
+from sklearn.pipeline import Pipeline
+
+from bee_migration.climate.observations import Observations
+from bee_migration.prediction.catalog import DEFAULT_CANDIDATES, Candidate
+from bee_migration.prediction.features import (
+ DEFAULT_TARGET,
+ FeatureTable,
+ Sample,
+ TrainTestSplit,
+)
+from bee_migration.prediction.scores import Evaluation
+
+DEFAULT_HOLDOUT_YEARS = 4
+TOP_FEATURES = 10
+
+
+@dataclass(frozen=True)
+class ModelResult:
+ """A fitted model with its accuracy and the features it relies on."""
+
+ name: str
+ pipeline: Pipeline
+ evaluation: Evaluation
+ importance: pd.Series
+
+ @classmethod
+ def trained(
+ cls, candidate: Candidate, split: TrainTestSplit, names: Sequence[str]
+ ) -> ModelResult:
+ pipeline = candidate.tuned(split.train)
+ evaluation = Evaluation(split.train.score(pipeline), split.test.score(pipeline))
+ return cls(
+ candidate.name,
+ pipeline,
+ evaluation,
+ split.train.importance(pipeline, names),
+ )
+
+ def test_r2(self) -> float:
+ return self.evaluation.test.r2
+
+ def describe(self) -> list[str]:
+ return [f"{self.name}:", *self.evaluation.test.describe("Test")]
+
+ def top_features(self) -> list[str]:
+ ranked = self.importance.sort_values(ascending=False).head(TOP_FEATURES)
+ return [f" {name}: {value:.4f}" for name, value in ranked.items()]
+
+ def forecast(self, sample: Sample) -> np.ndarray:
+ return self.pipeline.predict(sample.features)
+
+ def save(self, directory: Path) -> None:
+ slug = self.name.lower().replace(" ", "_")
+ joblib.dump(self.pipeline, directory / f"{slug}_model.pkl")
+
+
+class Leaderboard:
+ """Every trained model, comparable by accuracy on the held-out years."""
+
+ def __init__(self, results: Sequence[ModelResult]) -> None:
+ if not results:
+ raise ValueError("A leaderboard needs at least one trained model")
+ self._results = tuple(results)
+
+ def __iter__(self) -> Iterator[ModelResult]:
+ return iter(self._results)
+
+ def __len__(self) -> int:
+ return len(self._results)
+
+ def best(self) -> ModelResult:
+ return max(self._results, key=ModelResult.test_r2)
+
+ def metrics(self) -> pd.DataFrame:
+ rows = {result.name: result.evaluation.labelled() for result in self._results}
+ return pd.DataFrame.from_dict(rows, orient="index")
+
+ def importances(self) -> pd.DataFrame:
+ return pd.DataFrame(
+ {result.name: result.importance for result in self._results}
+ )
+
+ def forecasts(self, sample: Sample) -> pd.DataFrame:
+ """What every model predicts for the rows of `sample`."""
+ return pd.DataFrame(
+ {result.name: result.forecast(sample) for result in self._results}
+ )
+
+ def save(self, directory: Path) -> None:
+ directory.mkdir(parents=True, exist_ok=True)
+ for result in self._results:
+ result.save(directory)
+
+
+class MigrationPredictor:
+ """Trains candidate models to forecast a bee measurement from climate."""
+
+ def __init__(
+ self,
+ candidates: Sequence[Candidate] = DEFAULT_CANDIDATES,
+ holdout_years: int = DEFAULT_HOLDOUT_YEARS,
+ ) -> None:
+ self._candidates = tuple(candidates)
+ self._holdout_years = holdout_years
+
+ def train(
+ self, observations: Observations, target: str = DEFAULT_TARGET
+ ) -> Leaderboard:
+ features = FeatureTable(observations, target)
+ split = features.split(self._holdout_years)
+ names = features.names()
+ return Leaderboard(
+ [
+ ModelResult.trained(candidate, split, names)
+ for candidate in self._candidates
+ ]
+ )
diff --git a/src/bee_migration/reports/__init__.py b/src/bee_migration/reports/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/reports/analysis_report.py b/src/bee_migration/reports/analysis_report.py
new file mode 100644
index 0000000..8e64b15
--- /dev/null
+++ b/src/bee_migration/reports/analysis_report.py
@@ -0,0 +1,82 @@
+from pathlib import Path
+
+from bee_migration.analysis.correlation import Correlations
+from bee_migration.analysis.temporal import TemporalPatterns
+from bee_migration.climate.observations import Observations
+from bee_migration.reports.layout import banner, document, section
+
+LISTED_CORRELATIONS = 5
+
+
+class AnalysisReport:
+ """Plain-text summary of trends and correlations in the observations."""
+
+ def __init__(self, observations: Observations) -> None:
+ self._observations = observations
+
+ def text(self) -> str:
+ return document(
+ [
+ banner("ANALYSIS REPORT: BEES AND CLIMATE CHANGE"),
+ self._coverage(),
+ self._averages(),
+ self._trends(),
+ self._correlations(),
+ self._findings(),
+ ]
+ )
+
+ def save(self, path: Path) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(self.text(), encoding="utf-8")
+
+ def _coverage(self) -> str:
+ years = self._observations.table()["year"]
+ regions = ", ".join(self._observations.region_names())
+ return "\n".join(
+ [
+ f"Period analysed: {years.min()} - {years.max()}",
+ f"Regions analysed: {regions}",
+ f"Total records: {len(self._observations)}",
+ ]
+ )
+
+ def _averages(self) -> str:
+ means = self._observations.table().mean(numeric_only=True)
+ return section(
+ "DATA SUMMARY",
+ [
+ f"Mean temperature: {means['temperature']:.1f} °C",
+ f"Mean precipitation: {means['precipitation']:.0f} mm",
+ f"Mean bee abundance: {means['bee_abundance']:.1f}",
+ f"Mean species diversity: {means['species_diversity']:.1f}",
+ f"Mean honey production: {means['honey_production']:.1f} kg",
+ ],
+ )
+
+ def _trends(self) -> str:
+ trends = TemporalPatterns(self._observations).trends()
+ lines = [
+ f"{region}: temperature {row['temperature']:+.3f} °C/year, "
+ f"bee abundance {row['bee_abundance']:+.2f}/year"
+ for region, row in trends.iterrows()
+ ]
+ return section("REGIONAL TRENDS", lines)
+
+ def _correlations(self) -> str:
+ pairs = Correlations(self._observations).notable_pairs()
+ lines = [pair.describe() for pair in pairs[:LISTED_CORRELATIONS]]
+ return section("STRONGEST CORRELATIONS", lines)
+
+ def _findings(self) -> str:
+ trends = TemporalPatterns(self._observations).trends()
+ regions = len(trends)
+ warming = int((trends["temperature"] > 0).sum())
+ declining = int((trends["bee_abundance"] < 0).sum())
+ return section(
+ "KEY FINDINGS",
+ [
+ f"1. Warming in {warming} of {regions} regions",
+ f"2. Bee abundance declining in {declining} of {regions} regions",
+ ],
+ )
diff --git a/src/bee_migration/reports/layout.py b/src/bee_migration/reports/layout.py
new file mode 100644
index 0000000..7377bc9
--- /dev/null
+++ b/src/bee_migration/reports/layout.py
@@ -0,0 +1,16 @@
+from collections.abc import Sequence
+
+RULE = "=" * 80
+UNDERLINE = "-" * 40
+
+
+def banner(title: str) -> str:
+ return "\n".join([RULE, title, RULE])
+
+
+def section(title: str, lines: Sequence[str]) -> str:
+ return "\n".join([title, UNDERLINE, *lines])
+
+
+def document(parts: Sequence[str]) -> str:
+ return "\n\n".join(parts) + "\n"
diff --git a/src/bee_migration/reports/prediction_report.py b/src/bee_migration/reports/prediction_report.py
new file mode 100644
index 0000000..5787dcc
--- /dev/null
+++ b/src/bee_migration/reports/prediction_report.py
@@ -0,0 +1,30 @@
+from pathlib import Path
+
+from bee_migration.prediction.training import Leaderboard
+from bee_migration.reports.layout import banner, document, section
+
+
+class PredictionReport:
+ """Plain-text comparison of the trained models."""
+
+ def __init__(self, leaderboard: Leaderboard) -> None:
+ self._leaderboard = leaderboard
+
+ def text(self) -> str:
+ best = self._leaderboard.best()
+ performance = [
+ line for result in self._leaderboard for line in result.describe()
+ ]
+ return document(
+ [
+ banner("PREDICTION REPORT: MACHINE LEARNING"),
+ f"Models trained: {len(self._leaderboard)}",
+ section("PERFORMANCE ON HELD-OUT YEARS", performance),
+ section(f"MOST IMPORTANT FEATURES ({best.name})", best.top_features()),
+ section("RECOMMENDATION", [f"Best model: {best.name}"]),
+ ]
+ )
+
+ def save(self, path: Path) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(self.text(), encoding="utf-8")
diff --git a/src/bee_migration/sources/__init__.py b/src/bee_migration/sources/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/sources/gbif.py b/src/bee_migration/sources/gbif.py
new file mode 100644
index 0000000..8ad0bb3
--- /dev/null
+++ b/src/bee_migration/sources/gbif.py
@@ -0,0 +1,83 @@
+"""Bee occurrence records published through GBIF."""
+
+from collections.abc import Mapping
+from dataclasses import dataclass
+from typing import Any
+from urllib.parse import urlencode
+
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.period import StudyPeriod
+
+OCCURRENCES = "https://api.gbif.org/v1/occurrence/search"
+APIDAE = 4334
+CONTINENT = "SOUTH_AMERICA"
+SOUTHERN_TIP = -55.0
+NORTHERN_TIP = 15.0
+BAND_HEIGHT = 5.0
+MOST_YEARS = 200
+
+
+@dataclass(frozen=True)
+class LatitudeBand:
+ """A strip of the continent between two parallels."""
+
+ south: float
+ north: float
+
+ def centre(self) -> float:
+ return (self.south + self.north) / 2
+
+ def url(self, period: StudyPeriod) -> str:
+ """Address counting the bee records of this band for each year."""
+ query = {
+ "taxonKey": APIDAE,
+ "continent": CONTINENT,
+ "hasCoordinate": "true",
+ "decimalLatitude": f"{self.south},{self.north}",
+ "year": f"{period.first_year},{period.last_year}",
+ "facet": "year",
+ "facetLimit": MOST_YEARS,
+ "limit": 0,
+ }
+ return f"{OCCURRENCES}?{urlencode(query)}"
+
+
+def latitude_bands(
+ south: float = SOUTHERN_TIP,
+ north: float = NORTHERN_TIP,
+ height: float = BAND_HEIGHT,
+) -> list[LatitudeBand]:
+ edges = np.arange(south, north, height).tolist()
+ return [LatitudeBand(edge, edge + height) for edge in edges]
+
+
+def yearly_counts(answer: Mapping[str, Any]) -> dict[int, int]:
+ """Records per year from an occurrence search faceted by year."""
+ facets = [facet for facet in answer.get("facets", []) if facet["field"] == "YEAR"]
+ if not facets:
+ raise ValueError("GBIF answered without the yearly record counts")
+ return {int(entry["name"]): int(entry["count"]) for entry in facets[0]["counts"]}
+
+
+def record_centroids(
+ counts: Mapping[LatitudeBand, Mapping[int, int]],
+) -> pd.DataFrame:
+ """Records per year and their mean latitude, weighing each band by its records."""
+ table = pd.DataFrame(
+ {
+ band.centre(): pd.Series(yearly, dtype=float)
+ for band, yearly in counts.items()
+ }
+ ).fillna(0.0)
+ records = table.sum(axis=1)
+ weighted = (table * table.columns.to_numpy()).sum(axis=1)
+ centroids = pd.DataFrame(
+ {
+ "year": table.index.astype(int),
+ "records": records.astype(int).to_numpy(),
+ "mean_latitude": (weighted / records).to_numpy(),
+ }
+ )
+ return centroids.sort_values("year").reset_index(drop=True)
diff --git a/src/bee_migration/sources/open_meteo.py b/src/bee_migration/sources/open_meteo.py
new file mode 100644
index 0000000..0a66a65
--- /dev/null
+++ b/src/bee_migration/sources/open_meteo.py
@@ -0,0 +1,58 @@
+"""Historical weather from the Open-Meteo archive (ERA5 reanalysis)."""
+
+from collections.abc import Sequence
+from typing import Any
+from urllib.parse import urlencode
+
+import pandas as pd
+
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import RegionProfile
+
+ARCHIVE = "https://archive-api.open-meteo.com/v1/archive"
+# One variable keeps a 24-year request within the free per-minute allowance.
+DAILY_VARIABLE = "temperature_2m_mean"
+
+
+def archive_url(regions: Sequence[RegionProfile], period: StudyPeriod) -> str:
+ """Address of the daily temperature at every region's centre over the period."""
+ centres = [region.centre for region in regions]
+ query = {
+ "latitude": ",".join(str(centre.latitude) for centre in centres),
+ "longitude": ",".join(str(centre.longitude) for centre in centres),
+ "start_date": f"{period.first_year}-01-01",
+ "end_date": f"{period.last_year}-12-31",
+ "daily": DAILY_VARIABLE,
+ "timezone": "GMT",
+ }
+ return f"{ARCHIVE}?{urlencode(query)}"
+
+
+def yearly_climate(payload: Any, names: Sequence[str]) -> pd.DataFrame:
+ """Mean temperature per year for each named place."""
+ locations = _locations(payload)
+ if len(locations) != len(names):
+ raise ValueError(
+ f"Open-Meteo answered for {len(locations)} locations, expected {len(names)}"
+ )
+ yearly = [
+ _yearly(location, name) for location, name in zip(locations, names, strict=True)
+ ]
+ return pd.concat(yearly, ignore_index=True)
+
+
+def _locations(payload: Any) -> list[dict[str, Any]]:
+ if isinstance(payload, list):
+ return payload
+ if "daily" not in payload:
+ reason = payload.get("reason", "no daily values")
+ raise ValueError(f"Open-Meteo did not return temperatures: {reason}")
+ return [payload]
+
+
+def _yearly(location: dict[str, Any], name: str) -> pd.DataFrame:
+ daily = pd.DataFrame(location["daily"])
+ years = pd.to_datetime(daily["time"]).dt.year.rename("year")
+ readings = daily.groupby(years).agg(temperature=(DAILY_VARIABLE, "mean"))
+ labelled = readings.reset_index().assign(region=name)
+ return labelled[["year", "region", "temperature"]]
diff --git a/src/bee_migration/web/__init__.py b/src/bee_migration/web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/bee_migration/web/field.py b/src/bee_migration/web/field.py
new file mode 100644
index 0000000..99908aa
--- /dev/null
+++ b/src/bee_migration/web/field.py
@@ -0,0 +1,73 @@
+"""Field data as plain JSON: observed weather and where bees were recorded."""
+
+import json
+from typing import Any
+
+import numpy as np
+import pandas as pd
+
+from bee_migration.climate.period import DEFAULT_PERIOD
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.sources.gbif import (
+ LatitudeBand,
+ latitude_bands,
+ record_centroids,
+ yearly_counts,
+)
+from bee_migration.sources.open_meteo import archive_url, yearly_climate
+
+YEARS_PER_DECADE = 10
+DECIMALS = 4
+
+
+def field_requests() -> str:
+ """The addresses to fetch, as JSON, before calling `field_report`."""
+ bands = [
+ {"south": band.south, "north": band.north, "url": band.url(DEFAULT_PERIOD)}
+ for band in latitude_bands()
+ ]
+ return json.dumps(
+ {"climate": archive_url(BRAZILIAN_REGIONS, DEFAULT_PERIOD), "bands": bands}
+ )
+
+
+def field_report(document: str) -> str:
+ """Summarise the fetched answers: `climate` and one `answer` per band."""
+ fetched = json.loads(document)
+ names = [region.name for region in BRAZILIAN_REGIONS]
+ climate = yearly_climate(fetched["climate"], names)
+ centroids = record_centroids(
+ {
+ LatitudeBand(band["south"], band["north"]): yearly_counts(band["answer"])
+ for band in fetched["bands"]
+ }
+ )
+ return json.dumps(
+ {
+ "regions": sorted(names),
+ "climate": _records(climate),
+ "warming_per_decade": _warming(climate),
+ "centroids": _records(centroids),
+ "latitude_shift_per_decade": _per_decade(
+ centroids["year"], centroids["mean_latitude"]
+ ),
+ "total_records": int(centroids["records"].sum()),
+ }
+ )
+
+
+def _per_decade(years: pd.Series, values: pd.Series) -> float:
+ return float(np.polyfit(years, values, 1)[0]) * YEARS_PER_DECADE
+
+
+def _warming(climate: pd.DataFrame) -> float:
+ """Mean across regions of each region's temperature change per decade."""
+ changes = [
+ _per_decade(rows["year"], rows["temperature"])
+ for _, rows in climate.groupby("region")
+ ]
+ return float(np.mean(changes))
+
+
+def _records(table: pd.DataFrame) -> list[dict[str, Any]]:
+ return json.loads(table.to_json(orient="records", double_precision=DECIMALS + 2))
diff --git a/src/bee_migration/web/study.py b/src/bee_migration/web/study.py
new file mode 100644
index 0000000..249be07
--- /dev/null
+++ b/src/bee_migration/web/study.py
@@ -0,0 +1,109 @@
+"""A whole study as plain JSON, for interfaces that cannot hold Python objects."""
+
+from __future__ import annotations
+
+import json
+from dataclasses import asdict, dataclass, fields
+from typing import Any
+
+import pandas as pd
+
+from bee_migration.analysis.correlation import CORRELATED_COLUMNS, Correlations
+from bee_migration.analysis.temporal import TemporalPatterns
+from bee_migration.climate.observations import MEASUREMENTS, Observations
+from bee_migration.climate.period import DEFAULT_PERIOD
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.climate.simulation import DEFAULT_SEED, ClimateSimulation
+from bee_migration.prediction.catalog import DEFAULT_CANDIDATES, QUICK_CANDIDATES
+from bee_migration.prediction.features import DEFAULT_TARGET, FeatureTable
+from bee_migration.prediction.training import (
+ DEFAULT_HOLDOUT_YEARS,
+ TOP_FEATURES,
+ Leaderboard,
+ MigrationPredictor,
+)
+
+LISTED_PAIRS = 5
+LISTED_FEATURES = min(8, TOP_FEATURES)
+DECIMALS = 4
+
+
+@dataclass(frozen=True)
+class StudyRequest:
+ """The choices a visitor makes before running a study."""
+
+ seed: int = DEFAULT_SEED
+ holdout_years: int = DEFAULT_HOLDOUT_YEARS
+ target: str = DEFAULT_TARGET
+ thorough: bool = False
+
+ @classmethod
+ def parsed(cls, document: str) -> StudyRequest:
+ settings = json.loads(document)
+ unknown = set(settings) - {field.name for field in fields(cls)}
+ if unknown:
+ raise ValueError(f"Unknown study settings: {', '.join(sorted(unknown))}")
+ return cls(**settings)
+
+ def predictor(self) -> MigrationPredictor:
+ candidates = DEFAULT_CANDIDATES if self.thorough else QUICK_CANDIDATES
+ return MigrationPredictor(candidates, self.holdout_years)
+
+
+def run_study(document: str) -> str:
+ """Run the study described by a JSON request and answer in JSON."""
+ return json.dumps(simulated_study(StudyRequest.parsed(document)))
+
+
+def simulated_study(request: StudyRequest) -> dict[str, Any]:
+ simulation = ClimateSimulation(BRAZILIAN_REGIONS, DEFAULT_PERIOD)
+ observations = simulation.observations(request.seed)
+ return {
+ "request": asdict(request),
+ **described(observations),
+ **forecasted(observations, request),
+ }
+
+
+def described(observations: Observations) -> dict[str, Any]:
+ """What the observations show: values, trends and correlations."""
+ correlations = Correlations(observations)
+ trends = TemporalPatterns(observations).trends()
+ pairs = correlations.notable_pairs()[:LISTED_PAIRS]
+ return {
+ "regions": observations.region_names(),
+ "observations": _records(
+ observations.table()[["year", "region", *MEASUREMENTS]]
+ ),
+ "trends": _records(trends.reset_index(names="region")),
+ "correlations": {
+ "variables": CORRELATED_COLUMNS,
+ "matrix": correlations.matrix().round(DECIMALS).to_numpy().tolist(),
+ },
+ "pairs": [pair.record() for pair in pairs],
+ }
+
+
+def forecasted(observations: Observations, request: StudyRequest) -> dict[str, Any]:
+ """How well each model predicts the held-out years, and what drives it."""
+ features = FeatureTable(observations, request.target)
+ leaderboard = request.predictor().train(observations, request.target)
+ predictions = leaderboard.forecasts(features.split(request.holdout_years).test)
+ rows = pd.concat([features.holdout(request.holdout_years), predictions], axis=1)
+ return {
+ "models": _records(leaderboard.metrics().reset_index(names="model")),
+ "best": leaderboard.best().name,
+ "forecasts": _records(rows),
+ "importance": _importance(leaderboard),
+ }
+
+
+def _importance(leaderboard: Leaderboard) -> list[dict[str, Any]]:
+ ranked = leaderboard.best().importance.sort_values(ascending=False)
+ listed = ranked.head(LISTED_FEATURES).rename("importance")
+ return _records(listed.rename_axis("feature").reset_index())
+
+
+def _records(table: pd.DataFrame) -> list[dict[str, Any]]:
+ """Rows as JSON-safe dictionaries (numpy scalars become plain numbers)."""
+ return json.loads(table.to_json(orient="records", double_precision=DECIMALS + 2))
diff --git a/src/bee_migration/workspace.py b/src/bee_migration/workspace.py
new file mode 100644
index 0000000..87b6590
--- /dev/null
+++ b/src/bee_migration/workspace.py
@@ -0,0 +1,21 @@
+from pathlib import Path
+
+
+class Workspace:
+ """Where a project keeps its data, results and images."""
+
+ def __init__(self, root: Path) -> None:
+ self._root = root
+
+ def data(self, name: str) -> Path:
+ return self._writable(self._root / "data" / name)
+
+ def result(self, name: str) -> Path:
+ return self._writable(self._root / "results" / name)
+
+ def images(self) -> Path:
+ return self._root / "images"
+
+ def _writable(self, path: Path) -> Path:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ return path
diff --git a/src/bee_migration_analysis.py b/src/bee_migration_analysis.py
deleted file mode 100644
index 8e14e33..0000000
--- a/src/bee_migration_analysis.py
+++ /dev/null
@@ -1,399 +0,0 @@
-#!/usr/bin/env python3
-"""
-Análise de Migração de Abelhas para o Sul
-Baseado em estudos científicos sobre mudanças climáticas e migração de abelhas
-
-Fontes principais:
-- UFPR: Mudanças climáticas empurram abelhas sul-americanas para o Sul
-- Dryad Dataset: Climate-induced range shifts in south-eastern neotropics
-- PMC Studies: Global warming impacts on bee populations
-"""
-
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import seaborn as sns
-from pathlib import Path
-import logging
-from datetime import datetime
-from config import RESULTS_DIR, IMAGES_DIR, LOGGING_CONFIG
-
-class BeeMigrationAnalyzer:
- """
- Analisador de migração de abelhas baseado em evidências científicas
- """
-
- def __init__(self):
- self.results_dir = RESULTS_DIR
- self.images_dir = IMAGES_DIR
-
- # Configurar logging
- logging.basicConfig(**LOGGING_CONFIG)
- self.logger = logging.getLogger(__name__)
- self.results = {}
-
- def analyze_ufpr_findings(self):
- """
- Análise baseada no estudo da UFPR sobre migração de abelhas sul-americanas
-
- Principais descobertas:
- - 18 espécies estudadas no sudeste da América do Sul
- - Projeções para 2050 mostram migração para o sul
- - Bombus bellicosus: exemplo de extinção local em Curitiba
- - Perda de habitat no norte, ganhos no sul
- """
- self.logger.info("Iniciando análise baseada no estudo da UFPR")
-
- # Dados baseados no estudo da UFPR
- ufpr_data = {
- 'especies_estudadas': 18,
- 'regiao_estudo': 'Sudeste da América do Sul',
- 'projecao_ano': 2050,
- 'tendencia_migracao': 'Sul e Sudoeste',
- 'exemplo_extincao_local': {
- 'especie': 'Bombus bellicosus',
- 'local_anterior': 'Curitiba (anos 1960)',
- 'limite_atual': 'Palmas, Sul do Paraná',
- 'causa': 'Mudanças climáticas e alteração do uso do solo'
- },
- 'fatores_climaticos': [
- 'Aumento de temperatura',
- 'Mudanças na precipitação',
- 'Aquecimento global'
- ],
- 'impactos_observados': [
- 'Perda de habitat no norte',
- 'Ganhos de colonização ao sul',
- 'Extinções locais',
- 'Destruição de campos sulinos'
- ]
- }
-
- self.results['ufpr_analysis'] = ufpr_data
-
- # Criar visualização dos achados da UFPR
- self._plot_ufpr_migration_pattern()
-
- return ufpr_data
-
- def analyze_dryad_dataset_insights(self):
- """
- Análise baseada no dataset Dryad sobre mudanças climáticas
-
- Principais insights:
- - Avaliação de impactos climáticos em polinizadores selvagens
- - Foco na região neotropical sudeste
- - Análise de grupos funcionais diferentes
- - Projeções para 2050
- """
- self.logger.info("Analisando insights do dataset Dryad")
-
- dryad_insights = {
- 'objetivo': 'Avaliação de impactos climáticos em polinizadores selvagens',
- 'localizacao': 'Sudeste da América do Sul (SES)',
- 'taxon': 'Abelhas (Hymenoptera: Apidae sensu lato)',
- 'metodologia': 'Modelagem de distribuição de espécies',
- 'projecoes': {
- 'ano': 2050,
- 'tendencia_riqueza': 'Diminuição no norte, aumento no sul',
- 'direcao_migracao': 'Sudoeste e Sul',
- 'areas_estaveis': 'Sul do Brasil'
- },
- 'grupos_funcionais': {
- 'eussociais': 'Maiores perdas proporcionais',
- 'solitarias': 'Menores perdas',
- 'generalistas': 'Ganhos ligeiramente menores que especialistas'
- },
- 'conservacao': {
- 'areas_criticas': 'Campos naturais do sul do Brasil',
- 'habitat_ameacado': 'Campos sulinos',
- 'importancia': 'Alta diversidade de abelhas'
- }
- }
-
- self.results['dryad_analysis'] = dryad_insights
- return dryad_insights
-
- def analyze_global_warming_impacts(self):
- """
- Análise dos impactos do aquecimento global baseada nos estudos PMC
-
- Foco em:
- - Pragas de abelhas e aquecimento global
- - Reestruturação de comunidades de abelhas
- - Impactos em ilhas (Egeu)
- """
- self.logger.info("Analisando impactos globais do aquecimento")
-
- global_impacts = {
- 'small_hive_beetle': {
- 'especie': 'Aethina tumida',
- 'origem': 'África subsaariana',
- 'distribuicao_atual': 'Todos os continentes exceto Antártida',
- 'impacto_aquecimento': 'Aumento significativo da adequação climática',
- 'regioes_risco': 'Regiões temperadas do hemisfério norte',
- 'fatores_criticos': ['Temperatura do solo', 'Umidade do solo']
- },
- 'bumble_bees_north_america': {
- 'periodo_estudo': '50 anos',
- 'metrica': 'Índice de Temperatura Comunitária (CTI)',
- 'tendencia': 'Aumento de espécies adaptadas ao calor',
- 'impacto': 'Reestruturação rápida das comunidades',
- 'vulnerabilidade': 'Maiores latitudes e elevações'
- },
- 'aegean_islands': {
- 'regiao': 'Arquipélago do Egeu, Grécia',
- 'projecao': 'Contrações severas de distribuição',
- 'hotspots_atuais': 'NE do Egeu',
- 'mudanca_futura': 'Deslocamento altitudinal e latitudinal',
- 'conservacao': 'Rede Natura 2000 inadequada'
- }
- }
-
- self.results['global_impacts'] = global_impacts
- return global_impacts
-
- def _plot_ufpr_migration_pattern(self):
- """
- Cria visualização do padrão de migração baseado no estudo da UFPR
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 12))
- fig.suptitle('Análise de Migração de Abelhas - Estudo UFPR', fontsize=16, fontweight='bold')
-
- # 1. Timeline de extinção local - Bombus bellicosus
- years = [1960, 1990, 2024]
- presence = [1, 0.5, 0] # Presença em Curitiba
- ax1.plot(years, presence, 'ro-', linewidth=3, markersize=8)
- ax1.fill_between(years, presence, alpha=0.3, color='red')
- ax1.set_title('Extinção Local: Bombus bellicosus em Curitiba')
- ax1.set_xlabel('Ano')
- ax1.set_ylabel('Presença Relativa')
- ax1.set_ylim(0, 1.2)
- ax1.grid(True, alpha=0.3)
-
- # 2. Direção de migração projetada
- directions = ['Norte', 'Centro', 'Sul']
- habitat_change = [-30, 0, +25] # Mudança percentual de habitat adequado
- colors = ['red', 'yellow', 'green']
- bars = ax2.bar(directions, habitat_change, color=colors, alpha=0.7)
- ax2.set_title('Mudança Projetada de Habitat Adequado (2050)')
- ax2.set_ylabel('Mudança Percentual (%)')
- ax2.axhline(y=0, color='black', linestyle='-', alpha=0.5)
- ax2.grid(True, alpha=0.3)
-
- # Adicionar valores nas barras
- for bar, value in zip(bars, habitat_change):
- height = bar.get_height()
- ax2.text(bar.get_x() + bar.get_width()/2., height + (1 if height > 0 else -3),
- f'{value}%', ha='center', va='bottom' if height > 0 else 'top')
-
- # 3. Fatores de impacto
- factors = ['Temperatura', 'Precipitação', 'Uso do Solo', 'Monoculturas']
- impact_scores = [9, 7, 8, 6] # Escala de 1-10
- ax3.barh(factors, impact_scores, color='orange', alpha=0.7)
- ax3.set_title('Fatores de Impacto na Migração')
- ax3.set_xlabel('Intensidade do Impacto (1-10)')
- ax3.set_xlim(0, 10)
-
- # 4. Projeção temporal de migração
- projection_years = np.arange(2024, 2051, 5)
- migration_intensity = np.array([0, 15, 35, 60, 80, 100]) # Percentual de espécies migrando
- ax4.plot(projection_years, migration_intensity, 'bo-', linewidth=3, markersize=6)
- ax4.fill_between(projection_years, migration_intensity, alpha=0.3, color='blue')
- ax4.set_title('Projeção de Intensidade de Migração para o Sul')
- ax4.set_xlabel('Ano')
- ax4.set_ylabel('Espécies Migrando (%)')
- ax4.grid(True, alpha=0.3)
-
- plt.tight_layout()
-
- # Salvar gráfico
- output_path = self.images_dir / 'ufpr_migration_analysis.png'
- plt.savefig(output_path, dpi=300, bbox_inches='tight')
- plt.close()
-
- self.logger.info(f"Gráfico de migração UFPR salvo em: {output_path}")
-
- def generate_comprehensive_report(self):
- """
- Gera relatório abrangente sobre migração de abelhas
- """
- self.logger.info("Gerando relatório abrangente de migração")
-
- # Executar todas as análises
- ufpr_data = self.analyze_ufpr_findings()
- dryad_data = self.analyze_dryad_dataset_insights()
- global_data = self.analyze_global_warming_impacts()
-
- # Criar relatório
- report = f"""
-# RELATÓRIO DE ANÁLISE: MIGRAÇÃO DE ABELHAS PARA O SUL
-
-Data de Geração: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}
-
-## RESUMO EXECUTIVO
-
-Este relatório analisa evidências científicas sobre a migração de abelhas para o sul
-devido às mudanças climáticas, baseado em estudos recentes da UFPR, dataset Dryad
-e pesquisas internacionais.
-
-## 1. ESTUDO UFPR - ABELHAS SUL-AMERICANAS
-
-### Principais Descobertas:
-- **Espécies Estudadas**: {ufpr_data['especies_estudadas']} espécies no {ufpr_data['regiao_estudo']}
-- **Projeção Temporal**: Até {ufpr_data['projecao_ano']}
-- **Direção de Migração**: {ufpr_data['tendencia_migracao']}
-
-### Caso Emblemático - Bombus bellicosus:
-- **Situação Anterior**: Comum em {ufpr_data['exemplo_extincao_local']['local_anterior']}
-- **Situação Atual**: Limite norte em {ufpr_data['exemplo_extincao_local']['limite_atual']}
-- **Causa Principal**: {ufpr_data['exemplo_extincao_local']['causa']}
-
-### Fatores Climáticos Identificados:
-"""
-
- for factor in ufpr_data['fatores_climaticos']:
- report += f"- {factor}\n"
-
- report += f"""
-
-### Impactos Observados:
-"""
-
- for impact in ufpr_data['impactos_observados']:
- report += f"- {impact}\n"
-
- report += f"""
-
-## 2. ANÁLISE DRYAD - NEOTROPICAIS SUDESTE
-
-### Metodologia:
-- **Objetivo**: {dryad_data['objetivo']}
-- **Região**: {dryad_data['localizacao']}
-- **Abordagem**: {dryad_data['metodologia']}
-
-### Projeções para {dryad_data['projecoes']['ano']}:
-- **Tendência de Riqueza**: {dryad_data['projecoes']['tendencia_riqueza']}
-- **Direção de Migração**: {dryad_data['projecoes']['direcao_migracao']}
-- **Áreas Estáveis**: {dryad_data['projecoes']['areas_estaveis']}
-
-### Grupos Funcionais:
-- **Espécies Eussociais**: {dryad_data['grupos_funcionais']['eussociais']}
-- **Espécies Solitárias**: {dryad_data['grupos_funcionais']['solitarias']}
-- **Generalistas vs Especialistas**: {dryad_data['grupos_funcionais']['generalistas']}
-
-### Implicações para Conservação:
-- **Áreas Críticas**: {dryad_data['conservacao']['areas_criticas']}
-- **Habitat Ameaçado**: {dryad_data['conservacao']['habitat_ameacado']}
-- **Importância**: {dryad_data['conservacao']['importancia']}
-
-## 3. IMPACTOS GLOBAIS DO AQUECIMENTO
-
-### Small Hive Beetle (Aethina tumida):
-- **Origem**: {global_data['small_hive_beetle']['origem']}
-- **Distribuição Atual**: {global_data['small_hive_beetle']['distribuicao_atual']}
-- **Impacto do Aquecimento**: {global_data['small_hive_beetle']['impacto_aquecimento']}
-- **Regiões de Risco**: {global_data['small_hive_beetle']['regioes_risco']}
-
-### Abelhas Mamangavas - América do Norte:
-- **Período de Estudo**: {global_data['bumble_bees_north_america']['periodo_estudo']}
-- **Métrica Utilizada**: {global_data['bumble_bees_north_america']['metrica']}
-- **Tendência Observada**: {global_data['bumble_bees_north_america']['tendencia']}
-- **Impacto**: {global_data['bumble_bees_north_america']['impacto']}
-
-### Ilhas do Egeu:
-- **Região**: {global_data['aegean_islands']['regiao']}
-- **Projeção**: {global_data['aegean_islands']['projecao']}
-- **Hotspots Atuais**: {global_data['aegean_islands']['hotspots_atuais']}
-- **Mudança Futura**: {global_data['aegean_islands']['mudanca_futura']}
-
-## 4. SÍNTESE E CONCLUSÕES
-
-### Padrões Consistentes Identificados:
-1. **Migração Latitudinal**: Movimento consistente em direção aos polos
-2. **Perda de Habitat Norte**: Regiões mais quentes tornam-se inadequadas
-3. **Ganhos no Sul**: Expansão de habitat em regiões anteriormente frias
-4. **Vulnerabilidade Diferencial**: Grupos funcionais respondem diferentemente
-
-### Espécies Indicadoras:
-- **Bombus bellicosus**: Extinção local documentada (Curitiba)
-- **Small Hive Beetle**: Expansão facilitada pelo aquecimento
-- **Abelhas Neotropicais**: 18 espécies com migração projetada
-
-### Fatores Críticos:
-1. **Temperatura**: Principal driver da migração
-2. **Precipitação**: Alterações nos padrões de chuva
-3. **Uso do Solo**: Destruição de habitats naturais
-4. **Monoculturas**: Redução da diversidade de recursos
-
-### Recomendações:
-1. **Conservação Prioritária**: Campos sulinos do Brasil
-2. **Monitoramento**: Espécies indicadoras de mudança
-3. **Corredores Ecológicos**: Facilitar migração natural
-4. **Políticas Públicas**: Integrar mudanças climáticas na conservação
-
-## 5. LIMITAÇÕES E PESQUISAS FUTURAS
-
-### Limitações Identificadas:
-- Necessidade de validação de campo de longo prazo
-- Dados limitados para muitas espécies
-- Interações complexas entre fatores
-
-### Pesquisas Futuras Recomendadas:
-- Monitoramento de longo prazo de espécies indicadoras
-- Estudos de conectividade de habitat
-- Análise de redes de polinização
-- Impactos socioeconômicos da migração
-
----
-
-**Fontes:**
-- UFPR: Mudanças climáticas empurram abelhas sul-americanas para o Sul
-- Dryad Dataset: Climate-induced range shifts in south-eastern neotropics
-- PMC: Global warming promotes biological invasion of honey bee pest
-- Ecology Letters: Warming temperatures restructuring bumble bee communities
-- PMC: Climate change impacts on island bees (Aegean Archipelago)
-
-**Gerado por**: Bee Climate Analysis System
-**Versão**: 1.0
-"""
-
- # Salvar relatório
- report_path = self.results_dir / 'bee_migration_comprehensive_report.txt'
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write(report)
-
- self.logger.info(f"Relatório abrangente salvo em: {report_path}")
-
- return report
-
- def run_complete_analysis(self):
- """
- Executa análise completa de migração de abelhas
- """
- print("Iniciando Análise Abrangente de Migração de Abelhas")
- print("=" * 60)
-
- try:
- # Gerar relatório abrangente
- report = self.generate_comprehensive_report()
-
- print("\nAnálise Concluída com Sucesso!")
- print(f"Resultados salvos em: {self.results_dir}")
- print(f"Gráficos salvos em: {self.images_dir}")
-
- # Resumo dos achados principais
- print("\nPrincipais Descobertas:")
- print("- Migração consistente de abelhas para o sul devido ao aquecimento global")
- print("- Bombus bellicosus: extinção local documentada em Curitiba")
- print("- 18 espécies neotropicais com migração projetada até 2050")
- print("- Campos sulinos do Brasil identificados como áreas críticas de conservação")
- print("- Necessidade urgente de corredores ecológicos e políticas de conservação")
-
- except Exception as e:
- self.logger.error(f"Erro na análise: {str(e)}")
- raise
-
-if __name__ == "__main__":
- analyzer = BeeMigrationAnalyzer()
- analyzer.run_complete_analysis()
\ No newline at end of file
diff --git a/src/bee_migration_predictor.py b/src/bee_migration_predictor.py
deleted file mode 100644
index 9576463..0000000
--- a/src/bee_migration_predictor.py
+++ /dev/null
@@ -1,573 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Preditor de Migração de Abelhas usando Machine Learning.
-
-Este módulo implementa modelos de machine learning para prever padrões de migração
-de abelhas baseado em variáveis climáticas e ambientais.
-
-Autor: Diego Gomes
-Data: 2024
-Projeto: Bee Climate Analysis
-"""
-
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import seaborn as sns
-from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score
-from sklearn.preprocessing import StandardScaler, LabelEncoder
-from sklearn.linear_model import LinearRegression
-from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
-from sklearn.svm import SVR
-from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
-from sklearn.feature_selection import SelectKBest, f_regression
-import joblib
-import logging
-import warnings
-from pathlib import Path
-from typing import Dict, List, Tuple, Optional, Any
-
-# Imports locais
-from config import (
- ML_CONFIG, VISUALIZATION_CONFIG, RESULTS_DIR,
- IMAGES_DIR, PROCESSED_DATA_DIR
-)
-from bee_analysis import BeeClimateAnalyzer
-
-# Configurar warnings e logging
-warnings.filterwarnings('ignore')
-logger = logging.getLogger(__name__)
-
-class BeeMigrationPredictor:
- """
- Classe para predição de migração de abelhas usando machine learning.
- """
-
- def __init__(self):
- """
- Inicializa o preditor com configurações padrão.
- """
- self.data = None
- self.X_train = None
- self.X_test = None
- self.y_train = None
- self.y_test = None
- self.scaler = StandardScaler()
- self.models = {}
- self.best_models = {}
- self.feature_importance = {}
- self.predictions = {}
- self.metrics = {}
-
- logger.info("BeeMigrationPredictor inicializado")
-
- def load_and_prepare_data(self, data_path: Optional[str] = None) -> pd.DataFrame:
- """
- Carrega e prepara dados para machine learning.
-
- Args:
- data_path (str, optional): Caminho para arquivo de dados
-
- Returns:
- pd.DataFrame: Dados preparados
- """
- logger.info("Carregando e preparando dados para ML...")
-
- if data_path and Path(data_path).exists():
- self.data = pd.read_csv(data_path)
- else:
- # Usar dados do analisador principal
- analyzer = BeeClimateAnalyzer()
- self.data = analyzer.load_data()
-
- # Preparar features e targets
- self.data = self._engineer_features(self.data)
-
- logger.info(f"Dados preparados: {len(self.data)} registros, {self.data.shape[1]} features")
- return self.data
-
- def _engineer_features(self, data: pd.DataFrame) -> pd.DataFrame:
- """
- Engenharia de features para machine learning.
-
- Args:
- data (pd.DataFrame): Dados brutos
-
- Returns:
- pd.DataFrame: Dados com features engineered
- """
- logger.info("Realizando engenharia de features...")
-
- # Criar features temporais
- data['year_normalized'] = (data['year'] - data['year'].min()) / (data['year'].max() - data['year'].min())
- data['decade'] = (data['year'] // 10) * 10
-
- # Features climáticas derivadas
- data['temp_precip_ratio'] = data['temperature'] / (data['precipitation'] + 1)
- data['climate_extremity'] = np.sqrt(data['temperature_anomaly']**2 + data['precipitation_anomaly']**2)
-
- # Features de interação
- data['temp_lat_interaction'] = data['temperature'] * data['latitude']
- data['precip_lat_interaction'] = data['precipitation'] * data['latitude']
-
- # Features de lag (valores do ano anterior)
- data_sorted = data.sort_values(['region', 'year'])
- for col in ['temperature', 'precipitation', 'bee_abundance']:
- data_sorted[f'{col}_lag1'] = data_sorted.groupby('region')[col].shift(1)
- data_sorted[f'{col}_change'] = data_sorted[col] - data_sorted[f'{col}_lag1']
-
- # Features estatísticas por região
- for col in ['temperature', 'precipitation']:
- data_sorted[f'{col}_region_mean'] = data_sorted.groupby('region')[col].transform('mean')
- data_sorted[f'{col}_region_std'] = data_sorted.groupby('region')[col].transform('std')
- data_sorted[f'{col}_zscore'] = (data_sorted[col] - data_sorted[f'{col}_region_mean']) / data_sorted[f'{col}_region_std']
-
- # Encoding de variáveis categóricas
- le_region = LabelEncoder()
- data_sorted['region_encoded'] = le_region.fit_transform(data_sorted['region'])
-
- # Salvar encoder para uso posterior
- joblib.dump(le_region, RESULTS_DIR / 'region_encoder.pkl')
-
- return data_sorted.fillna(method='bfill').fillna(method='ffill')
-
- def prepare_ml_data(self, target_variable: str = 'bee_abundance') -> Tuple[np.ndarray, np.ndarray]:
- """
- Prepara dados para machine learning.
-
- Args:
- target_variable (str): Variável target para predição
-
- Returns:
- Tuple[np.ndarray, np.ndarray]: Features (X) e target (y)
- """
- logger.info(f"Preparando dados ML para target: {target_variable}")
-
- # Selecionar features
- feature_columns = [
- 'year_normalized', 'latitude', 'longitude', 'temperature', 'precipitation',
- 'temperature_anomaly', 'precipitation_anomaly', 'climate_stress_index',
- 'temp_precip_ratio', 'climate_extremity', 'temp_lat_interaction',
- 'precip_lat_interaction', 'region_encoded'
- ]
-
- # Adicionar features de lag se disponíveis
- lag_features = [col for col in self.data.columns if 'lag1' in col or 'change' in col or 'zscore' in col]
- feature_columns.extend(lag_features)
-
- # Remover features que não existem
- feature_columns = [col for col in feature_columns if col in self.data.columns]
-
- X = self.data[feature_columns].values
- y = self.data[target_variable].values
-
- # Remover linhas com NaN
- mask = ~(np.isnan(X).any(axis=1) | np.isnan(y))
- X = X[mask]
- y = y[mask]
-
- logger.info(f"Dados ML preparados: {X.shape[0]} amostras, {X.shape[1]} features")
-
- return X, y
-
- def split_data(self, X: np.ndarray, y: np.ndarray) -> None:
- """
- Divide dados em treino, validação e teste.
-
- Args:
- X (np.ndarray): Features
- y (np.ndarray): Target
- """
- config = ML_CONFIG['DATA_SPLIT']
-
- # Primeira divisão: treino+validação vs teste
- X_temp, self.X_test, y_temp, self.y_test = train_test_split(
- X, y,
- test_size=config['TEST_SIZE'],
- random_state=config['RANDOM_STATE'],
- stratify=None # Para regressão
- )
-
- # Segunda divisão: treino vs validação
- val_size = config['VALIDATION_SIZE'] / (config['TRAIN_SIZE'] + config['VALIDATION_SIZE'])
- self.X_train, self.X_val, self.y_train, self.y_val = train_test_split(
- X_temp, y_temp,
- test_size=val_size,
- random_state=config['RANDOM_STATE']
- )
-
- # Normalizar features
- self.X_train = self.scaler.fit_transform(self.X_train)
- self.X_val = self.scaler.transform(self.X_val)
- self.X_test = self.scaler.transform(self.X_test)
-
- logger.info(f"Dados divididos - Treino: {len(self.X_train)}, Validação: {len(self.X_val)}, Teste: {len(self.X_test)}")
-
- def initialize_models(self) -> Dict[str, Any]:
- """
- Inicializa modelos de machine learning.
-
- Returns:
- Dict[str, Any]: Dicionário com modelos inicializados
- """
- models = {}
-
- model_configs = ML_CONFIG['MODELS']
-
- if model_configs['LINEAR_REGRESSION']['enabled']:
- models['Linear Regression'] = LinearRegression()
-
- if model_configs['RANDOM_FOREST']['enabled']:
- models['Random Forest'] = RandomForestRegressor(
- random_state=ML_CONFIG['DATA_SPLIT']['RANDOM_STATE'],
- n_jobs=-1
- )
-
- if model_configs['GRADIENT_BOOSTING']['enabled']:
- models['Gradient Boosting'] = GradientBoostingRegressor(
- random_state=ML_CONFIG['DATA_SPLIT']['RANDOM_STATE']
- )
-
- if model_configs['SUPPORT_VECTOR_REGRESSION']['enabled']:
- models['SVR'] = SVR()
-
- self.models = models
- logger.info(f"Modelos inicializados: {list(models.keys())}")
-
- return models
-
- def train_models(self) -> Dict[str, Any]:
- """
- Treina todos os modelos com otimização de hiperparâmetros.
-
- Returns:
- Dict[str, Any]: Modelos treinados
- """
- logger.info("Iniciando treinamento dos modelos...")
-
- model_configs = ML_CONFIG['MODELS']
- cv_config = ML_CONFIG['CROSS_VALIDATION']
-
- for name, model in self.models.items():
- logger.info(f"Treinando {name}...")
-
- # Obter parâmetros para grid search
- if name == 'Random Forest' and model_configs['RANDOM_FOREST']['params']:
- param_grid = model_configs['RANDOM_FOREST']['params']
- elif name == 'Gradient Boosting' and model_configs['GRADIENT_BOOSTING']['params']:
- param_grid = model_configs['GRADIENT_BOOSTING']['params']
- elif name == 'SVR' and model_configs['SUPPORT_VECTOR_REGRESSION']['params']:
- param_grid = model_configs['SUPPORT_VECTOR_REGRESSION']['params']
- else:
- param_grid = {}
-
- if param_grid:
- # Grid search com validação cruzada
- grid_search = GridSearchCV(
- model,
- param_grid,
- cv=cv_config['cv_folds'],
- scoring=cv_config['scoring'],
- n_jobs=-1,
- verbose=0
- )
-
- grid_search.fit(self.X_train, self.y_train)
- self.best_models[name] = grid_search.best_estimator_
-
- logger.info(f"{name} - Melhores parâmetros: {grid_search.best_params_}")
- else:
- # Treinar modelo simples
- model.fit(self.X_train, self.y_train)
- self.best_models[name] = model
-
- logger.info("Treinamento concluído")
- return self.best_models
-
- def evaluate_models(self) -> Dict[str, Dict[str, float]]:
- """
- Avalia performance dos modelos.
-
- Returns:
- Dict[str, Dict[str, float]]: Métricas de cada modelo
- """
- logger.info("Avaliando performance dos modelos...")
-
- metrics = {}
-
- for name, model in self.best_models.items():
- # Predições
- y_pred_train = model.predict(self.X_train)
- y_pred_val = model.predict(self.X_val)
- y_pred_test = model.predict(self.X_test)
-
- # Salvar predições
- self.predictions[name] = {
- 'train': y_pred_train,
- 'validation': y_pred_val,
- 'test': y_pred_test
- }
-
- # Calcular métricas
- model_metrics = {}
-
- for dataset, y_true, y_pred in [
- ('train', self.y_train, y_pred_train),
- ('validation', self.y_val, y_pred_val),
- ('test', self.y_test, y_pred_test)
- ]:
- model_metrics[f'{dataset}_rmse'] = np.sqrt(mean_squared_error(y_true, y_pred))
- model_metrics[f'{dataset}_mae'] = mean_absolute_error(y_true, y_pred)
- model_metrics[f'{dataset}_r2'] = r2_score(y_true, y_pred)
-
- # MAPE (Mean Absolute Percentage Error)
- mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8))) * 100
- model_metrics[f'{dataset}_mape'] = mape
-
- metrics[name] = model_metrics
-
- logger.info(f"{name} - Test R²: {model_metrics['test_r2']:.3f}, Test RMSE: {model_metrics['test_rmse']:.3f}")
-
- self.metrics = metrics
- return metrics
-
- def analyze_feature_importance(self) -> Dict[str, np.ndarray]:
- """
- Analisa importância das features.
-
- Returns:
- Dict[str, np.ndarray]: Importância das features por modelo
- """
- logger.info("Analisando importância das features...")
-
- feature_names = [
- 'year_normalized', 'latitude', 'longitude', 'temperature', 'precipitation',
- 'temperature_anomaly', 'precipitation_anomaly', 'climate_stress_index',
- 'temp_precip_ratio', 'climate_extremity', 'temp_lat_interaction',
- 'precip_lat_interaction', 'region_encoded'
- ]
-
- # Adicionar nomes de features de lag
- lag_features = [col for col in self.data.columns if 'lag1' in col or 'change' in col or 'zscore' in col]
- feature_names.extend(lag_features)
-
- # Ajustar para o número real de features
- feature_names = feature_names[:self.X_train.shape[1]]
-
- importance = {}
-
- for name, model in self.best_models.items():
- if hasattr(model, 'feature_importances_'):
- # Modelos tree-based
- importance[name] = model.feature_importances_
- elif hasattr(model, 'coef_'):
- # Modelos lineares
- coef = np.abs(model.coef_)
- if coef.ndim > 1:
- coef = coef.flatten()
- importance[name] = coef
- else:
- # Para outros modelos, usar permutation importance
- from sklearn.inspection import permutation_importance
- perm_importance = permutation_importance(
- model, self.X_val, self.y_val, n_repeats=10, random_state=42
- )
- importance[name] = perm_importance.importances_mean
-
- self.feature_importance = importance
-
- # Criar DataFrame para facilitar análise
- importance_df = pd.DataFrame(importance, index=feature_names)
- importance_df.to_csv(RESULTS_DIR / 'feature_importance.csv')
-
- logger.info("Análise de importância concluída")
- return importance
-
- def generate_predictions_report(self) -> str:
- """
- Gera relatório detalhado das predições.
-
- Returns:
- str: Relatório em formato texto
- """
- logger.info("Gerando relatório de predições...")
-
- report = []
- report.append("=" * 80)
- report.append("RELATÓRIO DE PREDIÇÕES - MACHINE LEARNING")
- report.append("=" * 80)
- report.append(f"Data: {pd.Timestamp.now().strftime('%d/%m/%Y %H:%M')}")
- report.append(f"Modelos treinados: {len(self.best_models)}")
- report.append(f"Features utilizadas: {self.X_train.shape[1]}")
- report.append("")
-
- # Performance dos modelos
- report.append("PERFORMANCE DOS MODELOS")
- report.append("-" * 40)
-
- # Encontrar melhor modelo
- best_model_name = max(self.metrics.keys(),
- key=lambda x: self.metrics[x]['test_r2'])
-
- for name, metrics in self.metrics.items():
- marker = " ⭐" if name == best_model_name else ""
- report.append(f"{name}{marker}:")
- report.append(f" Test R²: {metrics['test_r2']:.4f}")
- report.append(f" Test RMSE: {metrics['test_rmse']:.4f}")
- report.append(f" Test MAE: {metrics['test_mae']:.4f}")
- report.append(f" Test MAPE: {metrics['test_mape']:.2f}%")
- report.append("")
-
- # Features mais importantes
- if self.feature_importance:
- report.append("FEATURES MAIS IMPORTANTES")
- report.append("-" * 40)
-
- # Usar o melhor modelo para mostrar importância
- if best_model_name in self.feature_importance:
- importance = self.feature_importance[best_model_name]
- feature_names = [
- 'year_normalized', 'latitude', 'longitude', 'temperature', 'precipitation',
- 'temperature_anomaly', 'precipitation_anomaly', 'climate_stress_index',
- 'temp_precip_ratio', 'climate_extremity', 'temp_lat_interaction',
- 'precip_lat_interaction', 'region_encoded'
- ]
-
- # Ajustar para o número real de features
- feature_names = feature_names[:len(importance)]
-
- # Ordenar por importância
- sorted_indices = np.argsort(importance)[::-1]
-
- for i, idx in enumerate(sorted_indices[:10]): # Top 10
- if idx < len(feature_names):
- report.append(f"{i+1:2d}. {feature_names[idx]}: {importance[idx]:.4f}")
-
- report.append("")
-
- # Recomendações
- report.append("RECOMENDAÇÕES")
- report.append("-" * 40)
- report.append(f"1. Melhor modelo: {best_model_name}")
- report.append(f"2. R² de {self.metrics[best_model_name]['test_r2']:.3f} indica {'boa' if self.metrics[best_model_name]['test_r2'] > 0.7 else 'moderada'} capacidade preditiva")
- report.append("3. Considerar coleta de mais dados para melhorar performance")
- report.append("4. Monitorar predições em dados futuros para validação")
- report.append("")
-
- report.append("=" * 80)
-
- report_text = "\n".join(report)
-
- # Salvar relatório
- report_path = RESULTS_DIR / "ml_predictions_report.txt"
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write(report_text)
-
- logger.info(f"Relatório de predições salvo em: {report_path}")
- return report_text
-
- def save_models(self):
- """
- Salva modelos treinados.
- """
- logger.info("Salvando modelos treinados...")
-
- models_dir = RESULTS_DIR / "models"
- models_dir.mkdir(exist_ok=True)
-
- for name, model in self.best_models.items():
- model_path = models_dir / f"{name.lower().replace(' ', '_')}_model.pkl"
- joblib.dump(model, model_path)
-
- # Salvar scaler
- joblib.dump(self.scaler, models_dir / "scaler.pkl")
-
- # Salvar métricas
- metrics_df = pd.DataFrame(self.metrics).T
- metrics_df.to_csv(RESULTS_DIR / "model_metrics.csv")
-
- logger.info(f"Modelos salvos em: {models_dir}")
-
- def run_complete_ml_pipeline(self, data_path: Optional[str] = None,
- target_variable: str = 'bee_abundance') -> Dict:
- """
- Executa pipeline completo de machine learning.
-
- Args:
- data_path (str, optional): Caminho para dados
- target_variable (str): Variável target
-
- Returns:
- Dict: Resultados completos do ML
- """
- logger.info("Iniciando pipeline completo de ML...")
-
- # Carregar e preparar dados
- self.load_and_prepare_data(data_path)
-
- # Preparar dados para ML
- X, y = self.prepare_ml_data(target_variable)
-
- # Dividir dados
- self.split_data(X, y)
-
- # Inicializar e treinar modelos
- self.initialize_models()
- self.train_models()
-
- # Avaliar modelos
- metrics = self.evaluate_models()
-
- # Analisar importância das features
- feature_importance = self.analyze_feature_importance()
-
- # Gerar relatório
- report = self.generate_predictions_report()
-
- # Salvar modelos
- self.save_models()
-
- logger.info("Pipeline de ML concluído")
-
- return {
- 'metrics': metrics,
- 'feature_importance': feature_importance,
- 'predictions': self.predictions,
- 'best_models': self.best_models,
- 'report': report
- }
-
-def main():
- """
- Função principal para executar predições ML.
- """
- print("Iniciando Predições de Migração de Abelhas com ML")
- print("=" * 60)
-
- # Criar preditor
- predictor = BeeMigrationPredictor()
-
- # Executar pipeline completo
- results = predictor.run_complete_ml_pipeline()
-
- # Exibir resumo
- print("\nRESUMO DOS RESULTADOS")
- print("-" * 30)
-
- best_model = max(results['metrics'].keys(),
- key=lambda x: results['metrics'][x]['test_r2'])
-
- print(f"Melhor modelo: {best_model}")
- print(f"R² no teste: {results['metrics'][best_model]['test_r2']:.4f}")
- print(f"RMSE no teste: {results['metrics'][best_model]['test_rmse']:.4f}")
- print(f"MAE no teste: {results['metrics'][best_model]['test_mae']:.4f}")
-
- print("\nPipeline de ML concluído com sucesso!")
- print(f"Modelos salvos em: {RESULTS_DIR / 'models'}")
- print(f"Métricas salvas em: {RESULTS_DIR}")
-
- return results
-
-if __name__ == "__main__":
- results = main()
\ No newline at end of file
diff --git a/src/comparative_analysis.py b/src/comparative_analysis.py
deleted file mode 100644
index 07e86b8..0000000
--- a/src/comparative_analysis.py
+++ /dev/null
@@ -1,537 +0,0 @@
-#!/usr/bin/env python3
-"""
-Análise Comparativa: Integração de Papers Científicos com Dados Locais
-Combina insights dos estudos científicos com análise dos dados do projeto
-
-Fontes integradas:
-- Dados locais do projeto (bee_climate_data.csv)
-- Estudo UFPR sobre migração sul-americana
-- Dataset Dryad sobre neotropicais
-- Estudos PMC sobre impactos globais
-"""
-
-import pandas as pd
-import numpy as np
-import matplotlib.pyplot as plt
-import seaborn as sns
-from pathlib import Path
-import logging
-from datetime import datetime
-from config import RESULTS_DIR, IMAGES_DIR, DATA_DIR, LOGGING_CONFIG
-
-class ComparativeAnalyzer:
- """
- Analisador comparativo que integra dados científicos com dados locais
- """
-
- def __init__(self):
- self.results_dir = RESULTS_DIR
- self.images_dir = IMAGES_DIR
- self.data_dir = DATA_DIR
-
- # Configurar logging
- logging.basicConfig(**LOGGING_CONFIG)
- self.logger = logging.getLogger(__name__)
-
- # Carregar dados locais
- self.local_data = self._load_local_data()
-
- # Dados dos papers científicos
- self.scientific_data = self._compile_scientific_data()
-
- def _load_local_data(self):
- """
- Carrega dados locais do projeto
- """
- try:
- # Tentar carregar dados processados primeiro
- processed_file = self.data_dir / 'processed' / 'bee_climate_processed.csv'
- if processed_file.exists():
- data = pd.read_csv(processed_file)
- self.logger.info(f"Dados processados carregados: {len(data)} registros")
- return data
-
- # Se não existir, carregar dados brutos
- raw_file = self.data_dir / 'bee_climate_data.csv'
- if raw_file.exists():
- data = pd.read_csv(raw_file)
- self.logger.info(f"Dados brutos carregados: {len(data)} registros")
- return data
-
- self.logger.warning("Nenhum arquivo de dados encontrado")
- return pd.DataFrame()
-
- except Exception as e:
- self.logger.error(f"Erro ao carregar dados locais: {str(e)}")
- return pd.DataFrame()
-
- def _compile_scientific_data(self):
- """
- Compila dados dos papers científicos em estrutura padronizada
- """
- return {
- 'ufpr_study': {
- 'species_count': 18,
- 'study_region': 'Southeast South America',
- 'projection_year': 2050,
- 'migration_direction': 'Southwest and South',
- 'key_species': 'Bombus bellicosus',
- 'extinction_location': 'Curitiba',
- 'current_limit': 'Palmas, South Paraná',
- 'habitat_loss_north': 30, # percentual
- 'habitat_gain_south': 25, # percentual
- 'temperature_increase': 2.5, # °C projetado
- 'precipitation_change': -15 # % mudança
- },
- 'dryad_dataset': {
- 'methodology': 'Species Distribution Modeling',
- 'functional_groups': {
- 'eusocial_loss': 35, # % perda
- 'solitary_loss': 20, # % perda
- 'generalist_gain': 15, # % ganho
- 'specialist_gain': 20 # % ganho
- },
- 'stable_areas': 'Southern Brazil',
- 'critical_habitat': 'Natural grasslands',
- 'biodiversity_hotspot': 'NE Aegean'
- },
- 'global_studies': {
- 'small_hive_beetle': {
- 'climate_suitability_increase': 60, # %
- 'risk_regions': 'Northern temperate zones',
- 'temperature_threshold': 25, # °C
- 'soil_moisture_critical': 40 # %
- },
- 'bumble_bees_na': {
- 'study_period': 50, # anos
- 'community_restructuring': 'Rapid',
- 'warm_adapted_increase': 45, # %
- 'cool_adapted_decrease': 35 # %
- },
- 'aegean_islands': {
- 'range_contraction': 70, # %
- 'altitudinal_shift': 200, # metros
- 'conservation_gap': 80 # %
- }
- }
- }
-
- def compare_temperature_trends(self):
- """
- Compara tendências de temperatura entre dados locais e estudos científicos
- """
- self.logger.info("Comparando tendências de temperatura")
-
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
- fig.suptitle('Comparação de Tendências de Temperatura: Dados Locais vs Estudos Científicos',
- fontsize=16, fontweight='bold')
-
- # 1. Projeções de temperatura por região
- regions = ['Norte\n(Dados Locais)', 'Centro\n(Dados Locais)', 'Sul\n(Dados Locais)',
- 'UFPR\nProjeção', 'Egeu\nProjeção']
- temp_increases = [3.2, 2.8, 2.1, 2.5, 3.5] # °C
- colors = ['red', 'orange', 'yellow', 'blue', 'purple']
-
- bars = ax1.bar(regions, temp_increases, color=colors, alpha=0.7)
- ax1.set_title('Aumento de Temperatura Projetado por Região')
- ax1.set_ylabel('Aumento de Temperatura (°C)')
- ax1.set_ylim(0, 4)
-
- # Adicionar valores nas barras
- for bar, value in zip(bars, temp_increases):
- height = bar.get_height()
- ax1.text(bar.get_x() + bar.get_width()/2., height + 0.05,
- f'{value}°C', ha='center', va='bottom')
-
- # 2. Timeline de mudanças observadas
- years = np.array([1960, 1990, 2000, 2010, 2020, 2024, 2030, 2040, 2050])
-
- # Bombus bellicosus (UFPR)
- bombus_presence = np.array([100, 50, 20, 5, 0, 0, 0, 0, 0]) # % presença em Curitiba
-
- # Dados locais simulados
- local_bee_abundance = np.array([100, 95, 90, 85, 80, 75, 70, 65, 60]) # % abundância relativa
-
- ax2.plot(years, bombus_presence, 'ro-', linewidth=3, label='Bombus bellicosus (Curitiba)', markersize=6)
- ax2.plot(years, local_bee_abundance, 'bo-', linewidth=3, label='Abelhas Locais (Projeção)', markersize=6)
- ax2.set_title('Declínio de Populações de Abelhas ao Longo do Tempo')
- ax2.set_xlabel('Ano')
- ax2.set_ylabel('Abundância Relativa (%)')
- ax2.legend()
- ax2.grid(True, alpha=0.3)
-
- # 3. Comparação de impactos por grupo funcional
- groups = ['Eussociais', 'Solitárias', 'Generalistas', 'Especialistas']
- dryad_impacts = [-35, -20, 15, 20] # % mudança
- local_impacts = [-25, -15, 10, 15] # % mudança estimada dos dados locais
-
- x = np.arange(len(groups))
- width = 0.35
-
- bars1 = ax3.bar(x - width/2, dryad_impacts, width, label='Estudo Dryad', alpha=0.7)
- bars2 = ax3.bar(x + width/2, local_impacts, width, label='Dados Locais (Est.)', alpha=0.7)
-
- ax3.set_title('Impactos por Grupo Funcional')
- ax3.set_ylabel('Mudança Populacional (%)')
- ax3.set_xticks(x)
- ax3.set_xticklabels(groups)
- ax3.legend()
- ax3.axhline(y=0, color='black', linestyle='-', alpha=0.5)
- ax3.grid(True, alpha=0.3)
-
- # 4. Mapa de calor de adequação climática
- # Simular dados de adequação climática por latitude
- latitudes = np.arange(-55, 13, 5) # América do Sul
- years_proj = np.arange(2020, 2055, 5)
-
- # Criar matriz de adequação climática (0-1)
- suitability_matrix = np.zeros((len(latitudes), len(years_proj)))
-
- for i, lat in enumerate(latitudes):
- for j, year in enumerate(years_proj):
- # Adequação diminui no norte e aumenta no sul com o tempo
- if lat < -30: # Sul
- suitability_matrix[i, j] = 0.6 + 0.3 * (j / len(years_proj))
- elif lat < 0: # Centro
- suitability_matrix[i, j] = 0.7 - 0.2 * (j / len(years_proj))
- else: # Norte
- suitability_matrix[i, j] = 0.8 - 0.5 * (j / len(years_proj))
-
- im = ax4.imshow(suitability_matrix, cmap='RdYlGn', aspect='auto', vmin=0, vmax=1)
- ax4.set_title('Adequação Climática Projetada por Latitude')
- ax4.set_xlabel('Ano')
- ax4.set_ylabel('Latitude')
- ax4.set_xticks(range(len(years_proj)))
- ax4.set_xticklabels(years_proj)
- ax4.set_yticks(range(0, len(latitudes), 3))
- ax4.set_yticklabels([f'{lat}°' for lat in latitudes[::3]])
-
- # Adicionar colorbar
- cbar = plt.colorbar(im, ax=ax4)
- cbar.set_label('Adequação Climática')
-
- plt.tight_layout()
-
- # Salvar gráfico
- output_path = self.images_dir / 'comparative_temperature_analysis.png'
- plt.savefig(output_path, dpi=300, bbox_inches='tight')
- plt.close()
-
- self.logger.info(f"Análise comparativa de temperatura salva em: {output_path}")
-
- def analyze_migration_patterns(self):
- """
- Analisa padrões de migração integrando múltiplas fontes
- """
- self.logger.info("Analisando padrões de migração integrados")
-
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
- fig.suptitle('Padrões de Migração de Abelhas: Síntese de Evidências Científicas',
- fontsize=16, fontweight='bold')
-
- # 1. Direções de migração por estudo
- studies = ['UFPR\n(18 spp)', 'Dryad\n(Neotropicais)', 'América do Norte\n(Bombus)', 'Egeu\n(Ilhas)']
- south_migration = [85, 90, 70, 60] # % espécies migrando para sul
- colors = ['green', 'blue', 'orange', 'purple']
-
- bars = ax1.bar(studies, south_migration, color=colors, alpha=0.7)
- ax1.set_title('Percentual de Espécies com Migração para o Sul')
- ax1.set_ylabel('Migração para Sul (%)')
- ax1.set_ylim(0, 100)
-
- for bar, value in zip(bars, south_migration):
- height = bar.get_height()
- ax1.text(bar.get_x() + bar.get_width()/2., height + 1,
- f'{value}%', ha='center', va='bottom')
-
- # 2. Velocidade de migração projetada
- years = np.arange(2024, 2051, 3)
-
- # Diferentes cenários de migração (ajustado para 9 pontos)
- conservative = np.array([0, 10, 25, 40, 55, 70, 80, 85, 90]) # % espécies migradas
- moderate = np.array([0, 15, 35, 55, 70, 80, 88, 92, 95])
- aggressive = np.array([0, 25, 50, 70, 85, 92, 96, 98, 99])
-
- ax2.plot(years, conservative, 'g-', linewidth=3, label='Cenário Conservador', marker='o')
- ax2.plot(years, moderate, 'b-', linewidth=3, label='Cenário Moderado', marker='s')
- ax2.plot(years, aggressive, 'r-', linewidth=3, label='Cenário Agressivo', marker='^')
-
- ax2.set_title('Projeções de Velocidade de Migração')
- ax2.set_xlabel('Ano')
- ax2.set_ylabel('Espécies que Migraram (%)')
- ax2.legend()
- ax2.grid(True, alpha=0.3)
-
- # 3. Fatores de impacto por região
- factors = ['Temperatura', 'Precipitação', 'Uso do Solo', 'Fragmentação', 'Pragas']
-
- # Impacto por região (escala 1-10)
- north_impact = [9, 7, 8, 9, 6]
- center_impact = [7, 6, 7, 7, 5]
- south_impact = [5, 4, 5, 4, 3]
-
- x = np.arange(len(factors))
- width = 0.25
-
- bars1 = ax3.bar(x - width, north_impact, width, label='Norte', color='red', alpha=0.7)
- bars2 = ax3.bar(x, center_impact, width, label='Centro', color='orange', alpha=0.7)
- bars3 = ax3.bar(x + width, south_impact, width, label='Sul', color='green', alpha=0.7)
-
- ax3.set_title('Intensidade de Fatores de Impacto por Região')
- ax3.set_ylabel('Intensidade do Impacto (1-10)')
- ax3.set_xticks(x)
- ax3.set_xticklabels(factors, rotation=45)
- ax3.legend()
- ax3.set_ylim(0, 10)
-
- # 4. Rede de conectividade de habitat
- # Simular conectividade entre regiões
- regions_net = ['Norte', 'Nordeste', 'Centro-Oeste', 'Sudeste', 'Sul']
- connectivity_matrix = np.array([
- [1.0, 0.3, 0.2, 0.1, 0.05], # Norte
- [0.3, 1.0, 0.4, 0.3, 0.1], # Nordeste
- [0.2, 0.4, 1.0, 0.6, 0.3], # Centro-Oeste
- [0.1, 0.3, 0.6, 1.0, 0.7], # Sudeste
- [0.05, 0.1, 0.3, 0.7, 1.0] # Sul
- ])
-
- im = ax4.imshow(connectivity_matrix, cmap='Greens', vmin=0, vmax=1)
- ax4.set_title('Conectividade de Habitat para Migração')
- ax4.set_xticks(range(len(regions_net)))
- ax4.set_yticks(range(len(regions_net)))
- ax4.set_xticklabels(regions_net, rotation=45)
- ax4.set_yticklabels(regions_net)
-
- # Adicionar valores na matriz
- for i in range(len(regions_net)):
- for j in range(len(regions_net)):
- text = ax4.text(j, i, f'{connectivity_matrix[i, j]:.2f}',
- ha="center", va="center", color="black" if connectivity_matrix[i, j] < 0.5 else "white")
-
- cbar = plt.colorbar(im, ax=ax4)
- cbar.set_label('Conectividade')
-
- plt.tight_layout()
-
- # Salvar gráfico
- output_path = self.images_dir / 'migration_patterns_synthesis.png'
- plt.savefig(output_path, dpi=300, bbox_inches='tight')
- plt.close()
-
- self.logger.info(f"Análise de padrões de migração salva em: {output_path}")
-
- def generate_integrated_report(self):
- """
- Gera relatório integrado combinando dados locais com estudos científicos
- """
- self.logger.info("Gerando relatório integrado")
-
- report = f"""
-# RELATÓRIO INTEGRADO: MIGRAÇÃO DE ABELHAS - DADOS LOCAIS vs EVIDÊNCIAS CIENTÍFICAS
-
-Data de Geração: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}
-
-## RESUMO EXECUTIVO
-
-Este relatório integra dados locais do projeto com evidências científicas de múltiplos
-estudos sobre migração de abelhas devido às mudanças climáticas, fornecendo uma
-visão abrangente e comparativa dos padrões observados.
-
-## 1. INTEGRAÇÃO DE DADOS
-
-### Dados Locais do Projeto:
-- **Registros Analisados**: {len(self.local_data) if not self.local_data.empty else 'Dados simulados'}
-- **Período Coberto**: 2000-2023
-- **Região de Foco**: América do Sul
-- **Espécies Monitoradas**: Múltiplas espécies nativas
-
-### Estudos Científicos Integrados:
-- **UFPR**: {self.scientific_data['ufpr_study']['species_count']} espécies, projeção até {self.scientific_data['ufpr_study']['projection_year']}
-- **Dryad Dataset**: Modelagem de distribuição de espécies neotropicais
-- **PMC Studies**: Impactos globais em abelhas e pragas
-- **Ecology Letters**: Reestruturação de comunidades na América do Norte
-
-## 2. CONVERGÊNCIA DE EVIDÊNCIAS
-
-### Padrões Consistentes Identificados:
-
-#### 2.1 Direção de Migração:
-- **UFPR**: {self.scientific_data['ufpr_study']['migration_direction']}
-- **Dryad**: Sudoeste e Sul (confirmado)
-- **Dados Locais**: Tendência similar observada
-- **Consenso**: 85-90% das espécies migram para o sul
-
-#### 2.2 Fatores Climáticos:
-- **Aumento de Temperatura**:
- - UFPR: {self.scientific_data['ufpr_study']['temperature_increase']}°C projetado
- - Dados Locais: 2.1-3.2°C observado/projetado
- - Consenso: 2-3°C de aumento crítico
-
-- **Mudanças na Precipitação**:
- - UFPR: {self.scientific_data['ufpr_study']['precipitation_change']}% de mudança
- - Padrão Global: Redução no norte, variabilidade no sul
-
-#### 2.3 Impactos por Grupo Funcional:
-- **Espécies Eussociais**: {self.scientific_data['dryad_dataset']['functional_groups']['eusocial_loss']}% de perda (Dryad)
-- **Espécies Solitárias**: {self.scientific_data['dryad_dataset']['functional_groups']['solitary_loss']}% de perda (Dryad)
-- **Dados Locais**: Padrão similar com variações regionais
-
-## 3. CASOS EMBLEMÁTICOS
-
-### 3.1 Bombus bellicosus (UFPR):
-- **Extinção Local**: {self.scientific_data['ufpr_study']['extinction_location']}
-- **Limite Atual**: {self.scientific_data['ufpr_study']['current_limit']}
-- **Implicação**: Indicador precoce de mudanças climáticas
-
-### 3.2 Small Hive Beetle (PMC):
-- **Expansão Climática**: {self.scientific_data['global_studies']['small_hive_beetle']['climate_suitability_increase']}% de aumento
-- **Regiões de Risco**: {self.scientific_data['global_studies']['small_hive_beetle']['risk_regions']}
-- **Relevância Local**: Potencial ameaça para abelhas sul-americanas
-
-### 3.3 Abelhas Norte-Americanas (Ecology Letters):
-- **Período de Estudo**: {self.scientific_data['global_studies']['bumble_bees_na']['study_period']} anos
-- **Reestruturação**: {self.scientific_data['global_studies']['bumble_bees_na']['community_restructuring']}
-- **Paralelo Local**: Padrões similares esperados na América do Sul
-
-## 4. PROJEÇÕES INTEGRADAS
-
-### 4.1 Cenários de Migração (2024-2050):
-
-#### Cenário Conservador:
-- 70-80% das espécies migram até 2050
-- Velocidade gradual de deslocamento
-- Adaptação parcial in situ
-
-#### Cenário Moderado (Mais Provável):
-- 85-95% das espécies migram até 2050
-- Velocidade moderada de deslocamento
-- Baseado na convergência dos estudos
-
-#### Cenário Agressivo:
-- 95-100% das espécies migram até 2040
-- Velocidade acelerada devido a eventos extremos
-- Colapso de populações no norte
-
-### 4.2 Áreas Críticas Identificadas:
-
-#### Áreas de Perda (Norte):
-- **Habitat Loss**: {self.scientific_data['ufpr_study']['habitat_loss_north']}% projetado
-- **Regiões**: Norte do Brasil, Venezuela, Colômbia
-- **Espécies Vulneráveis**: Especialistas e eussociais
-
-#### Áreas de Ganho (Sul):
-- **Habitat Gain**: {self.scientific_data['ufpr_study']['habitat_gain_south']}% projetado
-- **Regiões**: {self.scientific_data['dryad_dataset']['stable_areas']}
-- **Habitat Crítico**: {self.scientific_data['dryad_dataset']['critical_habitat']}
-
-## 5. VALIDAÇÃO CRUZADA
-
-### 5.1 Consistência entre Estudos:
-- **Alta Concordância**: Direção de migração (Sul/Sudoeste)
-- **Moderada Concordância**: Velocidade de migração
-- **Variação Regional**: Intensidade dos impactos
-
-### 5.2 Lacunas Identificadas:
-- Dados limitados para espécies tropicais
-- Falta de monitoramento de longo prazo
-- Interações entre fatores climáticos e não-climáticos
-
-## 6. RECOMENDAÇÕES INTEGRADAS
-
-### 6.1 Conservação Prioritária:
-1. **Proteção Imediata**: Campos sulinos do Brasil
-2. **Corredores Ecológicos**: Conectar norte-sul
-3. **Monitoramento**: Espécies indicadoras (ex: Bombus bellicosus)
-4. **Restauração**: Habitats degradados no sul
-
-### 6.2 Pesquisa Futura:
-1. **Validação de Campo**: Confirmar projeções com dados reais
-2. **Monitoramento Contínuo**: Estabelecer rede de observação
-3. **Modelagem Refinada**: Integrar mais variáveis ambientais
-4. **Estudos Socioeconômicos**: Impactos na polinização agrícola
-
-### 6.3 Políticas Públicas:
-1. **Planos de Adaptação**: Integrar migração de abelhas
-2. **Zoneamento Ecológico**: Considerar mudanças climáticas
-3. **Incentivos**: Conservação em propriedades privadas
-4. **Educação**: Conscientização sobre importância dos polinizadores
-
-## 7. CONCLUSÕES
-
-### Principais Achados:
-1. **Convergência Científica**: Múltiplos estudos confirmam migração para o sul
-2. **Urgência Temporal**: Mudanças já em curso, aceleração até 2050
-3. **Vulnerabilidade Diferencial**: Grupos funcionais respondem diferentemente
-4. **Oportunidade de Conservação**: Sul do Brasil como área crítica
-
-### Implicações:
-- **Ecológicas**: Reestruturação de comunidades de polinizadores
-- **Agrícolas**: Impactos na polinização de cultivos
-- **Conservacionistas**: Necessidade de estratégias adaptativas
-- **Socioeconômicas**: Custos de adaptação e perda de serviços
-
-### Próximos Passos:
-1. Implementar monitoramento sistemático
-2. Desenvolver estratégias de conservação adaptativa
-3. Integrar achados em políticas públicas
-4. Estabelecer colaborações internacionais
-
----
-
-**Metodologia de Integração:**
-Este relatório combina análise quantitativa de dados locais com revisão sistemática
-de literatura científica, utilizando métodos de síntese de evidências e validação cruzada.
-
-**Fontes Integradas:**
-- Dados locais do projeto Bee Climate Analysis
-- UFPR: Mudanças climáticas empurram abelhas sul-americanas para o Sul
-- Dryad Dataset: Climate-induced range shifts in south-eastern neotropics
-- PMC: Global warming promotes biological invasion of honey bee pest
-- Ecology Letters: Warming temperatures restructuring bumble bee communities
-- PMC: Climate change impacts on island bees (Aegean Archipelago)
-
-**Gerado por**: Comparative Analysis System
-**Versão**: 1.0
-**Confiabilidade**: Alta (múltiplas fontes convergentes)
-"""
-
- # Salvar relatório
- report_path = self.results_dir / 'integrated_migration_report.txt'
- with open(report_path, 'w', encoding='utf-8') as f:
- f.write(report)
-
- self.logger.info(f"Relatório integrado salvo em: {report_path}")
- return report
-
- def run_complete_analysis(self):
- """
- Executa análise comparativa completa
- """
- print("Iniciando Análise Comparativa Integrada")
- print("=" * 50)
-
- try:
- # Executar análises
- self.compare_temperature_trends()
- self.analyze_migration_patterns()
- report = self.generate_integrated_report()
-
- print("\nAnálise Comparativa Concluída com Sucesso!")
- print(f"Resultados salvos em: {self.results_dir}")
- print(f"Gráficos salvos em: {self.images_dir}")
-
- # Resumo dos achados principais
- print("\nPrincipais Descobertas da Integração:")
- print("- Convergência de 85-90% entre estudos sobre migração para o sul")
- print("- Bombus bellicosus como espécie indicadora validada")
- print("- Campos sulinos do Brasil confirmados como área crítica")
- print("- Necessidade urgente de corredores ecológicos norte-sul")
- print("- Validação cruzada confirma projeções até 2050")
-
- except Exception as e:
- self.logger.error(f"Erro na análise comparativa: {str(e)}")
- raise
-
-if __name__ == "__main__":
- analyzer = ComparativeAnalyzer()
- analyzer.run_complete_analysis()
\ No newline at end of file
diff --git a/src/config.py b/src/config.py
deleted file mode 100644
index 4fa9d2c..0000000
--- a/src/config.py
+++ /dev/null
@@ -1,293 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Configuração do projeto de análise de abelhas e mudanças climáticas.
-Centraliza todas as configurações, caminhos e parâmetros do projeto.
-
-Autor: Diego Gomes
-Data: 2024
-"""
-
-import os
-import logging
-from pathlib import Path
-
-# Diretórios base
-BASE_DIR = Path(__file__).parent.parent
-DATA_DIR = BASE_DIR / "data"
-RESULTS_DIR = BASE_DIR / "results"
-IMAGES_DIR = BASE_DIR / "images"
-DOCS_DIR = BASE_DIR / "docs"
-
-# Subdiretórios de dados
-RAW_DATA_DIR = DATA_DIR / "raw"
-PROCESSED_DATA_DIR = DATA_DIR / "processed"
-EXTERNAL_DATA_DIR = DATA_DIR / "external"
-
-# Criar diretórios se não existirem
-for directory in [DATA_DIR, RAW_DATA_DIR, PROCESSED_DATA_DIR, EXTERNAL_DATA_DIR,
- RESULTS_DIR, IMAGES_DIR, DOCS_DIR]:
- directory.mkdir(parents=True, exist_ok=True)
-
-# Configurações de dados
-DATA_CONFIG = {
- # URLs de APIs climáticas
- 'CLIMATE_APIS': {
- 'WORLDBANK': 'https://climateknowledgeportal.worldbank.org/api/data',
- 'NOAA': 'https://www.ncdc.noaa.gov/cdo-web/api/v2',
- 'ECMWF': 'https://cds.climate.copernicus.eu/api/v2'
- },
-
- # URLs de dados de biodiversidade
- 'BIODIVERSITY_APIS': {
- 'GBIF': 'https://api.gbif.org/v1',
- 'IUCN': 'https://apiv3.iucnredlist.org/api/v3',
- 'EOL': 'https://eol.org/api'
- },
-
- # Configurações de amostragem
- 'SAMPLING': {
- 'START_YEAR': 2000,
- 'END_YEAR': 2023,
- 'GEOGRAPHIC_BOUNDS': {
- 'NORTH': 12.0, # Venezuela
- 'SOUTH': -55.0, # Argentina
- 'WEST': -81.0, # Peru
- 'EAST': -34.0 # Brasil
- }
- },
-
- # Espécies de abelhas para análise
- 'TARGET_SPECIES': [
- 'Apis mellifera',
- 'Melipona quadrifasciata',
- 'Trigona spinipes',
- 'Scaptotrigona bipunctata',
- 'Nannotrigona testaceicornis'
- ]
-}
-
-# Configurações de análise
-ANALYSIS_CONFIG = {
- # Parâmetros de análise temporal
- 'TEMPORAL': {
- 'AGGREGATION_PERIODS': ['monthly', 'seasonal', 'yearly'],
- 'TREND_ANALYSIS_WINDOW': 5, # anos
- 'SEASONAL_DECOMPOSITION': True
- },
-
- # Parâmetros de análise espacial
- 'SPATIAL': {
- 'GRID_RESOLUTION': 0.5, # graus
- 'INTERPOLATION_METHOD': 'kriging',
- 'BUFFER_DISTANCE': 50 # km
- },
-
- # Parâmetros de correlação
- 'CORRELATION': {
- 'MIN_CORRELATION_THRESHOLD': 0.3,
- 'SIGNIFICANCE_LEVEL': 0.05,
- 'LAG_ANALYSIS_PERIODS': [0, 1, 2, 3, 6, 12] # meses
- }
-}
-
-# Configurações de Machine Learning
-ML_CONFIG = {
- # Divisão dos dados
- 'DATA_SPLIT': {
- 'TRAIN_SIZE': 0.7,
- 'VALIDATION_SIZE': 0.15,
- 'TEST_SIZE': 0.15,
- 'RANDOM_STATE': 42
- },
-
- # Modelos a serem testados
- 'MODELS': {
- 'LINEAR_REGRESSION': {
- 'enabled': True,
- 'params': {}
- },
- 'RANDOM_FOREST': {
- 'enabled': True,
- 'params': {
- 'n_estimators': [100, 200, 300],
- 'max_depth': [10, 20, None],
- 'min_samples_split': [2, 5, 10]
- }
- },
- 'GRADIENT_BOOSTING': {
- 'enabled': True,
- 'params': {
- 'n_estimators': [100, 200],
- 'learning_rate': [0.01, 0.1, 0.2],
- 'max_depth': [3, 5, 7]
- }
- },
- 'SUPPORT_VECTOR_REGRESSION': {
- 'enabled': True,
- 'params': {
- 'C': [0.1, 1, 10],
- 'gamma': ['scale', 'auto'],
- 'kernel': ['rbf', 'linear']
- }
- }
- },
-
- # Métricas de avaliação
- 'METRICS': ['rmse', 'mae', 'r2', 'mape'],
-
- # Validação cruzada
- 'CROSS_VALIDATION': {
- 'cv_folds': 5,
- 'scoring': 'neg_mean_squared_error'
- }
-}
-
-# Configurações de visualização
-VISUALIZATION_CONFIG = {
- # Configurações gerais de plots
- 'PLOT_STYLE': 'seaborn-v0_8',
- 'FIGURE_SIZE': (12, 8),
- 'DPI': 300,
- 'COLOR_PALETTE': 'husl',
-
- # Configurações específicas por tipo de gráfico
- 'CHART_CONFIGS': {
- 'correlation_heatmap': {
- 'cmap': 'RdYlBu_r',
- 'center': 0,
- 'annot': True
- },
- 'time_series': {
- 'linewidth': 2,
- 'marker': 'o',
- 'markersize': 6
- },
- 'scatter_plot': {
- 'alpha': 0.7,
- 's': 60
- },
- 'geographic_map': {
- 'projection': 'PlateCarree',
- 'resolution': '50m'
- }
- },
-
- # Configurações de salvamento
- 'SAVE_CONFIG': {
- 'format': 'png',
- 'bbox_inches': 'tight',
- 'transparent': False
- }
-}
-
-# Configurações de logging
-LOGGING_CONFIG = {
- 'level': logging.INFO,
- 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- 'filename': str(RESULTS_DIR / 'analysis.log')
-}
-
-# Configurações de performance
-PERFORMANCE_CONFIG = {
- 'PARALLEL_PROCESSING': {
- 'enabled': True,
- 'n_jobs': -1, # usar todos os cores disponíveis
- 'backend': 'loky'
- },
-
- 'MEMORY_OPTIMIZATION': {
- 'chunk_size': 10000,
- 'low_memory': True
- }
-}
-
-# Configurações específicas do projeto
-PROJECT_CONFIG = {
- 'NAME': 'Bee Climate Analysis',
- 'VERSION': '1.0.0',
- 'AUTHOR': 'Diego Gomes',
- 'DESCRIPTION': 'Análise do impacto das mudanças climáticas no comportamento de abelhas na América do Sul',
- 'GITHUB_URL': 'https://github.com/digomes87/Bees',
-
- # Configurações de relatório
- 'REPORT': {
- 'TITLE': 'Impacto das Mudanças Climáticas nas Abelhas Sul-Americanas',
- 'SUBTITLE': 'Análise Temporal e Espacial com Machine Learning',
- 'LANGUAGE': 'pt-BR',
- 'TEMPLATE': 'professional'
- }
-}
-
-# Função para validar configurações
-def validate_config():
- """
- Valida se todas as configurações estão corretas.
- """
- errors = []
-
- # Verificar se diretórios existem
- required_dirs = [DATA_DIR, RESULTS_DIR, IMAGES_DIR]
- for directory in required_dirs:
- if not directory.exists():
- errors.append(f"Diretório não encontrado: {directory}")
-
- # Verificar configurações de ML
- total_split = (ML_CONFIG['DATA_SPLIT']['TRAIN_SIZE'] +
- ML_CONFIG['DATA_SPLIT']['VALIDATION_SIZE'] +
- ML_CONFIG['DATA_SPLIT']['TEST_SIZE'])
-
- if abs(total_split - 1.0) > 0.001:
- errors.append("Soma das proporções de divisão dos dados deve ser 1.0")
-
- # Verificar bounds geográficos
- bounds = DATA_CONFIG['SAMPLING']['GEOGRAPHIC_BOUNDS']
- if bounds['NORTH'] <= bounds['SOUTH'] or bounds['EAST'] <= bounds['WEST']:
- errors.append("Bounds geográficos inválidos")
-
- if errors:
- raise ValueError(f"Erros de configuração encontrados: {'; '.join(errors)}")
-
- return True
-
-# Função para obter configuração específica
-def get_config(section, key=None):
- """
- Obtém configuração específica.
-
- Args:
- section (str): Seção da configuração
- key (str, optional): Chave específica dentro da seção
-
- Returns:
- dict or any: Configuração solicitada
- """
- config_map = {
- 'data': DATA_CONFIG,
- 'analysis': ANALYSIS_CONFIG,
- 'ml': ML_CONFIG,
- 'visualization': VISUALIZATION_CONFIG,
- 'logging': LOGGING_CONFIG,
- 'performance': PERFORMANCE_CONFIG,
- 'project': PROJECT_CONFIG
- }
-
- if section not in config_map:
- raise ValueError(f"Seção de configuração '{section}' não encontrada")
-
- config = config_map[section]
-
- if key is None:
- return config
-
- if key not in config:
- raise ValueError(f"Chave '{key}' não encontrada na seção '{section}'")
-
- return config[key]
-
-if __name__ == "__main__":
- # Validar configurações ao importar
- validate_config()
- print("✅ Configurações validadas com sucesso!")
- print(f"📁 Diretório base: {BASE_DIR}")
- print(f"📊 Projeto: {PROJECT_CONFIG['NAME']} v{PROJECT_CONFIG['VERSION']}")
\ No newline at end of file
diff --git a/src/generate_charts.py b/src/generate_charts.py
deleted file mode 100644
index bf06265..0000000
--- a/src/generate_charts.py
+++ /dev/null
@@ -1,397 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Script para gerar gráficos visuais demonstrativos sobre análise de abelhas e mudanças climáticas.
-Os gráficos são salvos na pasta images/ para exibição no README.
-
-Autor: Diego Gomes
-Data: 2024
-"""
-
-import numpy as np
-import pandas as pd
-import matplotlib.pyplot as plt
-import seaborn as sns
-from datetime import datetime, timedelta
-import warnings
-warnings.filterwarnings('ignore')
-
-# Configuração do estilo dos gráficos
-plt.style.use('seaborn-v0_8')
-sns.set_palette("husl")
-plt.rcParams['figure.figsize'] = (12, 8)
-plt.rcParams['font.size'] = 12
-plt.rcParams['axes.grid'] = True
-plt.rcParams['grid.alpha'] = 0.3
-
-def generate_sample_data():
- """
- Gera dados simulados realistas para demonstração das análises.
- """
- np.random.seed(42)
-
- # Dados temporais (2000-2023)
- years = np.arange(2000, 2024)
- n_years = len(years)
-
- # Dados de temperatura (tendência de aquecimento)
- base_temp = 22.5
- temp_trend = np.linspace(0, 2.5, n_years) # Aumento de 2.5°C em 23 anos
- temperature = base_temp + temp_trend + np.random.normal(0, 0.5, n_years)
-
- # Dados de latitude média das abelhas (migração para o sul)
- base_lat = -15.0 # Latitude inicial (sul do Brasil)
- lat_trend = np.linspace(0, -3.0, n_years) # Migração 3° para o sul
- latitude = base_lat + lat_trend + np.random.normal(0, 0.3, n_years)
-
- # Número de espécies observadas
- base_species = 45
- species_decline = np.linspace(0, -8, n_years) # Declínio de 8 espécies
- species_count = base_species + species_decline + np.random.normal(0, 2, n_years)
- species_count = np.maximum(species_count, 20) # Mínimo de 20 espécies
-
- # Produtividade de mel (kg/colmeia)
- base_honey = 25.0
- honey_decline = np.linspace(0, -5, n_years) # Declínio de 5kg
- honey_production = base_honey + honey_decline + np.random.normal(0, 1.5, n_years)
- honey_production = np.maximum(honey_production, 15) # Mínimo de 15kg
-
- return pd.DataFrame({
- 'year': years,
- 'temperature': temperature,
- 'latitude': latitude,
- 'species_count': species_count,
- 'honey_production': honey_production
- })
-
-def generate_geographic_data():
- """
- Gera dados geográficos simulados para mapas de distribuição.
- """
- np.random.seed(42)
-
- # Coordenadas da América do Sul
- n_points = 200
-
- # Distribuição atual (mais ao norte)
- lat_current = np.random.normal(-12, 4, n_points//2)
- lon_current = np.random.normal(-55, 8, n_points//2)
-
- # Distribuição futura (mais ao sul)
- lat_future = np.random.normal(-18, 4, n_points//2)
- lon_future = np.random.normal(-58, 8, n_points//2)
-
- current_data = pd.DataFrame({
- 'latitude': lat_current,
- 'longitude': lon_current,
- 'period': 'Atual (2020-2023)',
- 'abundance': np.random.exponential(2, n_points//2)
- })
-
- future_data = pd.DataFrame({
- 'latitude': lat_future,
- 'longitude': lon_future,
- 'period': 'Projetado (2040-2050)',
- 'abundance': np.random.exponential(1.5, n_points//2)
- })
-
- return pd.concat([current_data, future_data], ignore_index=True)
-
-def plot_temperature_migration_trend(df):
- """
- Gráfico mostrando a correlação entre temperatura e migração das abelhas.
- """
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))
-
- # Gráfico 1: Temperatura ao longo do tempo
- ax1.plot(df['year'], df['temperature'], 'o-', color='red', linewidth=2, markersize=6)
- ax1.set_title('Aumento da Temperatura Média Anual na América do Sul', fontsize=16, fontweight='bold')
- ax1.set_xlabel('Ano')
- ax1.set_ylabel('Temperatura (°C)')
- ax1.grid(True, alpha=0.3)
-
- # Linha de tendência
- z = np.polyfit(df['year'], df['temperature'], 1)
- p = np.poly1d(z)
- ax1.plot(df['year'], p(df['year']), "--", color='darkred', alpha=0.8, linewidth=2)
-
- # Gráfico 2: Latitude média das abelhas
- ax2.plot(df['year'], df['latitude'], 'o-', color='blue', linewidth=2, markersize=6)
- ax2.set_title('Migração das Abelhas para o Sul (Latitude Média)', fontsize=16, fontweight='bold')
- ax2.set_xlabel('Ano')
- ax2.set_ylabel('Latitude (°S)')
- ax2.grid(True, alpha=0.3)
-
- # Linha de tendência
- z2 = np.polyfit(df['year'], df['latitude'], 1)
- p2 = np.poly1d(z2)
- ax2.plot(df['year'], p2(df['year']), "--", color='darkblue', alpha=0.8, linewidth=2)
-
- plt.tight_layout()
- plt.savefig('../images/temperature_migration_trend.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Gráfico salvo: temperature_migration_trend.png")
-
-def plot_correlation_analysis(df):
- """
- Matriz de correlação e scatter plots.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
-
- # Matriz de correlação
- corr_matrix = df[['temperature', 'latitude', 'species_count', 'honey_production']].corr()
- sns.heatmap(corr_matrix, annot=True, cmap='RdYlBu_r', center=0,
- square=True, ax=ax1, cbar_kws={'shrink': 0.8})
- ax1.set_title('Matriz de Correlação entre Variáveis', fontsize=14, fontweight='bold')
-
- # Scatter plot: Temperatura vs Latitude
- ax2.scatter(df['temperature'], df['latitude'], alpha=0.7, s=60, color='purple')
- ax2.set_xlabel('Temperatura (°C)')
- ax2.set_ylabel('Latitude (°S)')
- ax2.set_title('Temperatura vs Latitude das Abelhas')
-
- # Linha de regressão
- z = np.polyfit(df['temperature'], df['latitude'], 1)
- p = np.poly1d(z)
- ax2.plot(df['temperature'], p(df['temperature']), "r--", alpha=0.8)
-
- # Scatter plot: Temperatura vs Número de Espécies
- ax3.scatter(df['temperature'], df['species_count'], alpha=0.7, s=60, color='green')
- ax3.set_xlabel('Temperatura (°C)')
- ax3.set_ylabel('Número de Espécies')
- ax3.set_title('Temperatura vs Diversidade de Espécies')
-
- # Linha de regressão
- z3 = np.polyfit(df['temperature'], df['species_count'], 1)
- p3 = np.poly1d(z3)
- ax3.plot(df['temperature'], p3(df['temperature']), "r--", alpha=0.8)
-
- # Scatter plot: Temperatura vs Produção de Mel
- ax4.scatter(df['temperature'], df['honey_production'], alpha=0.7, s=60, color='orange')
- ax4.set_xlabel('Temperatura (°C)')
- ax4.set_ylabel('Produção de Mel (kg/colmeia)')
- ax4.set_title('Temperatura vs Produção de Mel')
-
- # Linha de regressão
- z4 = np.polyfit(df['temperature'], df['honey_production'], 1)
- p4 = np.poly1d(z4)
- ax4.plot(df['temperature'], p4(df['temperature']), "r--", alpha=0.8)
-
- plt.tight_layout()
- plt.savefig('../images/correlation_analysis.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Gráfico salvo: correlation_analysis.png")
-
-def plot_species_decline(df):
- """
- Gráfico mostrando o declínio das espécies e produção de mel.
- """
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
-
- # Declínio das espécies
- ax1.fill_between(df['year'], df['species_count'], alpha=0.3, color='red')
- ax1.plot(df['year'], df['species_count'], 'o-', color='darkred', linewidth=2, markersize=5)
- ax1.set_title('Declínio na Diversidade de Espécies de Abelhas', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Ano')
- ax1.set_ylabel('Número de Espécies Observadas')
- ax1.grid(True, alpha=0.3)
-
- # Linha de tendência
- z = np.polyfit(df['year'], df['species_count'], 1)
- p = np.poly1d(z)
- ax1.plot(df['year'], p(df['year']), "--", color='black', alpha=0.8, linewidth=2)
-
- # Produção de mel
- ax2.fill_between(df['year'], df['honey_production'], alpha=0.3, color='gold')
- ax2.plot(df['year'], df['honey_production'], 'o-', color='darkorange', linewidth=2, markersize=5)
- ax2.set_title('Impacto na Produção de Mel', fontsize=14, fontweight='bold')
- ax2.set_xlabel('Ano')
- ax2.set_ylabel('Produção Média (kg/colmeia)')
- ax2.grid(True, alpha=0.3)
-
- # Linha de tendência
- z2 = np.polyfit(df['year'], df['honey_production'], 1)
- p2 = np.poly1d(z2)
- ax2.plot(df['year'], p2(df['year']), "--", color='black', alpha=0.8, linewidth=2)
-
- plt.tight_layout()
- plt.savefig('../images/species_honey_decline.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Gráfico salvo: species_honey_decline.png")
-
-def plot_geographic_distribution(geo_df):
- """
- Mapa mostrando a distribuição atual vs futura das abelhas.
- """
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 8))
-
- # Distribuição atual
- current = geo_df[geo_df['period'] == 'Atual (2020-2023)']
- scatter1 = ax1.scatter(current['longitude'], current['latitude'],
- c=current['abundance'], s=60, alpha=0.7,
- cmap='Reds', edgecolors='black', linewidth=0.5)
- ax1.set_title('Distribuição Atual das Abelhas\n(2020-2023)', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Longitude')
- ax1.set_ylabel('Latitude')
- ax1.grid(True, alpha=0.3)
-
- # Distribuição futura
- future = geo_df[geo_df['period'] == 'Projetado (2040-2050)']
- scatter2 = ax2.scatter(future['longitude'], future['latitude'],
- c=future['abundance'], s=60, alpha=0.7,
- cmap='Blues', edgecolors='black', linewidth=0.5)
- ax2.set_title('Distribuição Projetada das Abelhas\n(2040-2050)', fontsize=14, fontweight='bold')
- ax2.set_xlabel('Longitude')
- ax2.set_ylabel('Latitude')
- ax2.grid(True, alpha=0.3)
-
- # Colorbar
- plt.colorbar(scatter1, ax=ax1, label='Abundância Relativa')
- plt.colorbar(scatter2, ax=ax2, label='Abundância Relativa')
-
- plt.tight_layout()
- plt.savefig('../images/geographic_distribution.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Gráfico salvo: geographic_distribution.png")
-
-def plot_climate_impact_summary(df):
- """
- Gráfico resumo mostrando todos os impactos das mudanças climáticas.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
-
- # Normalizar dados para comparação (0-100)
- temp_norm = ((df['temperature'] - df['temperature'].min()) /
- (df['temperature'].max() - df['temperature'].min())) * 100
-
- lat_norm = ((df['latitude'].max() - df['latitude']) /
- (df['latitude'].max() - df['latitude'].min())) * 100
-
- species_norm = ((df['species_count'] - df['species_count'].min()) /
- (df['species_count'].max() - df['species_count'].min())) * 100
-
- honey_norm = ((df['honey_production'] - df['honey_production'].min()) /
- (df['honey_production'].max() - df['honey_production'].min())) * 100
-
- # Gráfico 1: Tendências normalizadas
- ax1.plot(df['year'], temp_norm, 'o-', label='Temperatura', color='red', linewidth=2)
- ax1.plot(df['year'], lat_norm, 'o-', label='Migração Sul', color='blue', linewidth=2)
- ax1.plot(df['year'], species_norm, 'o-', label='Diversidade', color='green', linewidth=2)
- ax1.plot(df['year'], honey_norm, 'o-', label='Produção Mel', color='orange', linewidth=2)
- ax1.set_title('Tendências Normalizadas (0-100)', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Ano')
- ax1.set_ylabel('Índice Normalizado')
- ax1.legend()
- ax1.grid(True, alpha=0.3)
-
- # Gráfico 2: Boxplot das variáveis
- data_for_box = [df['temperature'], df['latitude'].abs(), df['species_count'], df['honey_production']]
- labels_box = ['Temperatura\n(°C)', 'Latitude\n(°S)', 'Espécies\n(count)', 'Mel\n(kg)']
-
- box_plot = ax2.boxplot(data_for_box, labels=labels_box, patch_artist=True)
- colors = ['red', 'blue', 'green', 'orange']
- for patch, color in zip(box_plot['boxes'], colors):
- patch.set_facecolor(color)
- patch.set_alpha(0.7)
-
- ax2.set_title('Distribuição das Variáveis', fontsize=14, fontweight='bold')
- ax2.grid(True, alpha=0.3)
-
- # Gráfico 3: Mudança percentual desde 2000
- base_year = df.iloc[0]
- temp_change = ((df['temperature'] - base_year['temperature']) / base_year['temperature']) * 100
- species_change = ((df['species_count'] - base_year['species_count']) / base_year['species_count']) * 100
- honey_change = ((df['honey_production'] - base_year['honey_production']) / base_year['honey_production']) * 100
-
- ax3.plot(df['year'], temp_change, 'o-', label='Temperatura', color='red', linewidth=2)
- ax3.plot(df['year'], species_change, 'o-', label='Diversidade', color='green', linewidth=2)
- ax3.plot(df['year'], honey_change, 'o-', label='Produção Mel', color='orange', linewidth=2)
- ax3.axhline(y=0, color='black', linestyle='--', alpha=0.5)
- ax3.set_title('Mudança Percentual desde 2000', fontsize=14, fontweight='bold')
- ax3.set_xlabel('Ano')
- ax3.set_ylabel('Mudança (%)')
- ax3.legend()
- ax3.grid(True, alpha=0.3)
-
- # Gráfico 4: Projeção futura (2024-2030)
- future_years = np.arange(2024, 2031)
-
- # Extrapolação linear simples
- temp_trend = np.polyfit(df['year'], df['temperature'], 1)
- species_trend = np.polyfit(df['year'], df['species_count'], 1)
- honey_trend = np.polyfit(df['year'], df['honey_production'], 1)
-
- future_temp = np.polyval(temp_trend, future_years)
- future_species = np.polyval(species_trend, future_years)
- future_honey = np.polyval(honey_trend, future_years)
-
- # Normalizar projeções
- all_temp = np.concatenate([df['temperature'], future_temp])
- all_species = np.concatenate([df['species_count'], future_species])
- all_honey = np.concatenate([df['honey_production'], future_honey])
-
- temp_norm_proj = ((all_temp - all_temp.min()) / (all_temp.max() - all_temp.min())) * 100
- species_norm_proj = ((all_species - all_species.min()) / (all_species.max() - all_species.min())) * 100
- honey_norm_proj = ((all_honey - all_honey.min()) / (all_honey.max() - all_honey.min())) * 100
-
- all_years = np.concatenate([df['year'], future_years])
-
- ax4.plot(all_years[:len(df)], temp_norm_proj[:len(df)], 'o-', color='red', linewidth=2, label='Temperatura')
- ax4.plot(all_years[:len(df)], species_norm_proj[:len(df)], 'o-', color='green', linewidth=2, label='Diversidade')
- ax4.plot(all_years[:len(df)], honey_norm_proj[:len(df)], 'o-', color='orange', linewidth=2, label='Produção Mel')
-
- ax4.plot(all_years[len(df):], temp_norm_proj[len(df):], '--', color='red', linewidth=2, alpha=0.7)
- ax4.plot(all_years[len(df):], species_norm_proj[len(df):], '--', color='green', linewidth=2, alpha=0.7)
- ax4.plot(all_years[len(df):], honey_norm_proj[len(df):], '--', color='orange', linewidth=2, alpha=0.7)
-
- ax4.axvline(x=2023, color='black', linestyle=':', alpha=0.7, label='Projeção')
- ax4.set_title('Projeção até 2030', fontsize=14, fontweight='bold')
- ax4.set_xlabel('Ano')
- ax4.set_ylabel('Índice Normalizado')
- ax4.legend()
- ax4.grid(True, alpha=0.3)
-
- plt.tight_layout()
- plt.savefig('../images/climate_impact_summary.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Gráfico salvo: climate_impact_summary.png")
-
-def main():
- """
- Função principal que executa todas as análises e gera os gráficos.
- """
- print("Iniciando geração de gráficos para análise de abelhas...\n")
-
- # Gerar dados simulados
- print("Gerando dados simulados...")
- df = generate_sample_data()
- geo_df = generate_geographic_data()
-
- print(f"Dados gerados: {len(df)} anos de dados temporais")
- print(f"Dados geográficos: {len(geo_df)} pontos de distribuição\n")
-
- # Gerar gráficos
- print("Gerando gráficos...")
-
- plot_temperature_migration_trend(df)
- plot_correlation_analysis(df)
- plot_species_decline(df)
- plot_geographic_distribution(geo_df)
- plot_climate_impact_summary(df)
-
- print("\nTodos os gráficos foram gerados com sucesso!")
- print("Arquivos salvos na pasta 'images/'")
- print("\nGráficos gerados:")
- print(" 1. temperature_migration_trend.png - Tendência de temperatura e migração")
- print(" 2. correlation_analysis.png - Análise de correlações")
- print(" 3. species_honey_decline.png - Declínio de espécies e produção")
- print(" 4. geographic_distribution.png - Distribuição geográfica")
- print(" 5. climate_impact_summary.png - Resumo dos impactos climáticos")
-
- # Salvar dados para uso posterior
- df.to_csv('../data/bee_climate_data.csv', index=False)
- geo_df.to_csv('../data/bee_geographic_data.csv', index=False)
- print("\nDados salvos em 'data/' para análises futuras")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/generate_charts_english.py b/src/generate_charts_english.py
deleted file mode 100644
index d9fa0d1..0000000
--- a/src/generate_charts_english.py
+++ /dev/null
@@ -1,647 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-Script to generate visual charts for bee migration and climate change analysis.
-Charts are saved in the images/ folder for README display.
-
-Author: Diego Gomes
-Date: 2024
-"""
-
-import numpy as np
-import pandas as pd
-import matplotlib.pyplot as plt
-import seaborn as sns
-from datetime import datetime, timedelta
-import warnings
-warnings.filterwarnings('ignore')
-
-# Chart style configuration
-plt.style.use('seaborn-v0_8')
-sns.set_palette("husl")
-plt.rcParams['figure.figsize'] = (12, 8)
-plt.rcParams['font.size'] = 12
-plt.rcParams['axes.grid'] = True
-plt.rcParams['grid.alpha'] = 0.3
-
-def generate_sample_data():
- """
- Generates realistic simulated data for analysis demonstration.
- """
- np.random.seed(42)
-
- # Temporal data (2000-2023)
- years = np.arange(2000, 2024)
- n_years = len(years)
-
- # Temperature data (warming trend)
- base_temp = 22.5
- temp_trend = np.linspace(0, 2.5, n_years) # 2.5°C increase over 23 years
- temperature = base_temp + temp_trend + np.random.normal(0, 0.5, n_years)
-
- # Average bee latitude (southward migration)
- base_lat = -15.0 # Initial latitude (southern Brazil)
- lat_trend = np.linspace(0, -3.0, n_years) # 3° southward migration
- latitude = base_lat + lat_trend + np.random.normal(0, 0.3, n_years)
-
- # Number of observed species
- base_species = 45
- species_decline = np.linspace(0, -8, n_years) # Decline of 8 species
- species_count = base_species + species_decline + np.random.normal(0, 2, n_years)
- species_count = np.maximum(species_count, 20) # Minimum 20 species
-
- # Honey productivity (kg/hive)
- base_honey = 25.0
- honey_decline = np.linspace(0, -5, n_years) # 5kg decline
- honey_production = base_honey + honey_decline + np.random.normal(0, 1.5, n_years)
- honey_production = np.maximum(honey_production, 15) # Minimum 15kg
-
- return pd.DataFrame({
- 'year': years,
- 'temperature': temperature,
- 'latitude': latitude,
- 'species_count': species_count,
- 'honey_production': honey_production
- })
-
-def generate_geographic_data():
- """
- Generates simulated geographic data for distribution maps.
- """
- np.random.seed(42)
-
- # South American coordinates
- n_points = 200
-
- # Current distribution (more northern)
- lat_current = np.random.normal(-12, 4, n_points//2)
- lon_current = np.random.normal(-55, 8, n_points//2)
-
- # Future distribution (more southern)
- lat_future = np.random.normal(-18, 4, n_points//2)
- lon_future = np.random.normal(-58, 8, n_points//2)
-
- current_data = pd.DataFrame({
- 'latitude': lat_current,
- 'longitude': lon_current,
- 'period': 'Current (2020-2023)',
- 'abundance': np.random.exponential(2, n_points//2)
- })
-
- future_data = pd.DataFrame({
- 'latitude': lat_future,
- 'longitude': lon_future,
- 'period': 'Projected (2040-2050)',
- 'abundance': np.random.exponential(1.5, n_points//2)
- })
-
- return pd.concat([current_data, future_data], ignore_index=True)
-
-def plot_temperature_migration_trend(df):
- """
- Chart showing correlation between temperature and bee migration.
- """
- fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10))
-
- # Chart 1: Temperature over time
- ax1.plot(df['year'], df['temperature'], 'o-', color='red', linewidth=2, markersize=6)
- ax1.set_title('Annual Average Temperature Increase in South America', fontsize=16, fontweight='bold')
- ax1.set_xlabel('Year')
- ax1.set_ylabel('Temperature (°C)')
- ax1.grid(True, alpha=0.3)
-
- # Trend line
- z = np.polyfit(df['year'], df['temperature'], 1)
- p = np.poly1d(z)
- ax1.plot(df['year'], p(df['year']), "--", color='darkred', alpha=0.8, linewidth=2)
-
- # Chart 2: Average bee latitude
- ax2.plot(df['year'], df['latitude'], 'o-', color='blue', linewidth=2, markersize=6)
- ax2.set_title('Southward Bee Migration (Average Latitude)', fontsize=16, fontweight='bold')
- ax2.set_xlabel('Year')
- ax2.set_ylabel('Latitude (°S)')
- ax2.grid(True, alpha=0.3)
-
- # Trend line
- z2 = np.polyfit(df['year'], df['latitude'], 1)
- p2 = np.poly1d(z2)
- ax2.plot(df['year'], p2(df['year']), "--", color='darkblue', alpha=0.8, linewidth=2)
-
- plt.tight_layout()
- plt.savefig('../images/temperature_migration_trend.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: temperature_migration_trend.png")
-
-def plot_correlation_analysis(df):
- """
- Correlation matrix and scatter plots.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
-
- # Correlation matrix
- corr_matrix = df[['temperature', 'latitude', 'species_count', 'honey_production']].corr()
- sns.heatmap(corr_matrix, annot=True, cmap='RdYlBu_r', center=0,
- square=True, ax=ax1, cbar_kws={'shrink': 0.8})
- ax1.set_title('Correlation Matrix Between Variables', fontsize=14, fontweight='bold')
-
- # Scatter plot: Temperature vs Latitude
- ax2.scatter(df['temperature'], df['latitude'], alpha=0.7, s=60, color='purple')
- ax2.set_xlabel('Temperature (°C)')
- ax2.set_ylabel('Latitude (°S)')
- ax2.set_title('Temperature vs Bee Latitude')
-
- # Regression line
- z = np.polyfit(df['temperature'], df['latitude'], 1)
- p = np.poly1d(z)
- ax2.plot(df['temperature'], p(df['temperature']), "r--", alpha=0.8)
-
- # Scatter plot: Temperature vs Number of Species
- ax3.scatter(df['temperature'], df['species_count'], alpha=0.7, s=60, color='green')
- ax3.set_xlabel('Temperature (°C)')
- ax3.set_ylabel('Number of Species')
- ax3.set_title('Temperature vs Species Diversity')
-
- # Regression line
- z3 = np.polyfit(df['temperature'], df['species_count'], 1)
- p3 = np.poly1d(z3)
- ax3.plot(df['temperature'], p3(df['temperature']), "r--", alpha=0.8)
-
- # Scatter plot: Temperature vs Honey Production
- ax4.scatter(df['temperature'], df['honey_production'], alpha=0.7, s=60, color='orange')
- ax4.set_xlabel('Temperature (°C)')
- ax4.set_ylabel('Honey Production (kg/hive)')
- ax4.set_title('Temperature vs Honey Production')
-
- # Regression line
- z4 = np.polyfit(df['temperature'], df['honey_production'], 1)
- p4 = np.poly1d(z4)
- ax4.plot(df['temperature'], p4(df['temperature']), "r--", alpha=0.8)
-
- plt.tight_layout()
- plt.savefig('../images/correlation_analysis.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: correlation_analysis.png")
-
-def plot_species_decline(df):
- """
- Chart showing species decline and honey production.
- """
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
-
- # Species decline
- ax1.fill_between(df['year'], df['species_count'], alpha=0.3, color='red')
- ax1.plot(df['year'], df['species_count'], 'o-', color='darkred', linewidth=2, markersize=5)
- ax1.set_title('Decline in Bee Species Diversity', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Year')
- ax1.set_ylabel('Number of Observed Species')
- ax1.grid(True, alpha=0.3)
-
- # Trend line
- z = np.polyfit(df['year'], df['species_count'], 1)
- p = np.poly1d(z)
- ax1.plot(df['year'], p(df['year']), "--", color='black', alpha=0.8, linewidth=2)
-
- # Honey production
- ax2.fill_between(df['year'], df['honey_production'], alpha=0.3, color='gold')
- ax2.plot(df['year'], df['honey_production'], 'o-', color='darkorange', linewidth=2, markersize=5)
- ax2.set_title('Impact on Honey Production', fontsize=14, fontweight='bold')
- ax2.set_xlabel('Year')
- ax2.set_ylabel('Average Production (kg/hive)')
- ax2.grid(True, alpha=0.3)
-
- # Trend line
- z2 = np.polyfit(df['year'], df['honey_production'], 1)
- p2 = np.poly1d(z2)
- ax2.plot(df['year'], p2(df['year']), "--", color='black', alpha=0.8, linewidth=2)
-
- plt.tight_layout()
- plt.savefig('../images/species_honey_decline.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: species_honey_decline.png")
-
-def plot_geographic_distribution(geo_df):
- """
- Map showing current vs future bee distribution.
- """
- fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 8))
-
- # Current distribution
- current = geo_df[geo_df['period'] == 'Current (2020-2023)']
- scatter1 = ax1.scatter(current['longitude'], current['latitude'],
- c=current['abundance'], s=60, alpha=0.7,
- cmap='Reds', edgecolors='black', linewidth=0.5)
- ax1.set_title('Current Bee Distribution\n(2020-2023)', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Longitude')
- ax1.set_ylabel('Latitude')
- ax1.grid(True, alpha=0.3)
-
- # Future distribution
- future = geo_df[geo_df['period'] == 'Projected (2040-2050)']
- scatter2 = ax2.scatter(future['longitude'], future['latitude'],
- c=future['abundance'], s=60, alpha=0.7,
- cmap='Blues', edgecolors='black', linewidth=0.5)
- ax2.set_title('Projected Bee Distribution\n(2040-2050)', fontsize=14, fontweight='bold')
- ax2.set_xlabel('Longitude')
- ax2.set_ylabel('Latitude')
- ax2.grid(True, alpha=0.3)
-
- # Colorbar
- plt.colorbar(scatter1, ax=ax1, label='Relative Abundance')
- plt.colorbar(scatter2, ax=ax2, label='Relative Abundance')
-
- plt.tight_layout()
- plt.savefig('../images/geographic_distribution.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: geographic_distribution.png")
-
-def plot_climate_impact_summary(df):
- """
- Summary chart showing all climate change impacts.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
-
- # Normalize data for comparison (0-100)
- temp_norm = ((df['temperature'] - df['temperature'].min()) /
- (df['temperature'].max() - df['temperature'].min())) * 100
-
- lat_norm = ((df['latitude'].max() - df['latitude']) /
- (df['latitude'].max() - df['latitude'].min())) * 100
-
- species_norm = ((df['species_count'] - df['species_count'].min()) /
- (df['species_count'].max() - df['species_count'].min())) * 100
-
- honey_norm = ((df['honey_production'] - df['honey_production'].min()) /
- (df['honey_production'].max() - df['honey_production'].min())) * 100
-
- # Chart 1: Normalized trends
- ax1.plot(df['year'], temp_norm, 'o-', label='Temperature', color='red', linewidth=2)
- ax1.plot(df['year'], lat_norm, 'o-', label='Southward Migration', color='blue', linewidth=2)
- ax1.plot(df['year'], species_norm, 'o-', label='Diversity', color='green', linewidth=2)
- ax1.plot(df['year'], honey_norm, 'o-', label='Honey Production', color='orange', linewidth=2)
- ax1.set_title('Normalized Trends (0-100)', fontsize=14, fontweight='bold')
- ax1.set_xlabel('Year')
- ax1.set_ylabel('Normalized Index')
- ax1.legend()
- ax1.grid(True, alpha=0.3)
-
- # Chart 2: Variable distribution boxplot
- data_for_box = [df['temperature'], df['latitude'].abs(), df['species_count'], df['honey_production']]
- labels_box = ['Temperature\n(°C)', 'Latitude\n(°S)', 'Species\n(count)', 'Honey\n(kg)']
-
- box_plot = ax2.boxplot(data_for_box, labels=labels_box, patch_artist=True)
- colors = ['red', 'blue', 'green', 'orange']
- for patch, color in zip(box_plot['boxes'], colors):
- patch.set_facecolor(color)
- patch.set_alpha(0.7)
-
- ax2.set_title('Variable Distribution', fontsize=14, fontweight='bold')
- ax2.grid(True, alpha=0.3)
-
- # Chart 3: Percentage change since 2000
- base_year = df.iloc[0]
- temp_change = ((df['temperature'] - base_year['temperature']) / base_year['temperature']) * 100
- species_change = ((df['species_count'] - base_year['species_count']) / base_year['species_count']) * 100
- honey_change = ((df['honey_production'] - base_year['honey_production']) / base_year['honey_production']) * 100
-
- ax3.plot(df['year'], temp_change, 'o-', label='Temperature', color='red', linewidth=2)
- ax3.plot(df['year'], species_change, 'o-', label='Diversity', color='green', linewidth=2)
- ax3.plot(df['year'], honey_change, 'o-', label='Honey Production', color='orange', linewidth=2)
- ax3.axhline(y=0, color='black', linestyle='--', alpha=0.5)
- ax3.set_title('Percentage Change Since 2000', fontsize=14, fontweight='bold')
- ax3.set_xlabel('Year')
- ax3.set_ylabel('Change (%)')
- ax3.legend()
- ax3.grid(True, alpha=0.3)
-
- # Chart 4: Future projection (2024-2030)
- future_years = np.arange(2024, 2031)
-
- # Simple linear extrapolation
- temp_trend = np.polyfit(df['year'], df['temperature'], 1)
- species_trend = np.polyfit(df['year'], df['species_count'], 1)
- honey_trend = np.polyfit(df['year'], df['honey_production'], 1)
-
- future_temp = np.polyval(temp_trend, future_years)
- future_species = np.polyval(species_trend, future_years)
- future_honey = np.polyval(honey_trend, future_years)
-
- # Normalize projections
- all_temp = np.concatenate([df['temperature'], future_temp])
- all_species = np.concatenate([df['species_count'], future_species])
- all_honey = np.concatenate([df['honey_production'], future_honey])
-
- temp_norm_proj = ((all_temp - all_temp.min()) / (all_temp.max() - all_temp.min())) * 100
- species_norm_proj = ((all_species - all_species.min()) / (all_species.max() - all_species.min())) * 100
- honey_norm_proj = ((all_honey - all_honey.min()) / (all_honey.max() - all_honey.min())) * 100
-
- all_years = np.concatenate([df['year'], future_years])
-
- ax4.plot(all_years[:len(df)], temp_norm_proj[:len(df)], 'o-', color='red', linewidth=2, label='Temperature')
- ax4.plot(all_years[:len(df)], species_norm_proj[:len(df)], 'o-', color='green', linewidth=2, label='Diversity')
- ax4.plot(all_years[:len(df)], honey_norm_proj[:len(df)], 'o-', color='orange', linewidth=2, label='Honey Production')
-
- ax4.plot(all_years[len(df):], temp_norm_proj[len(df):], '--', color='red', linewidth=2, alpha=0.7)
- ax4.plot(all_years[len(df):], species_norm_proj[len(df):], '--', color='green', linewidth=2, alpha=0.7)
- ax4.plot(all_years[len(df):], honey_norm_proj[len(df):], '--', color='orange', linewidth=2, alpha=0.7)
-
- ax4.axvline(x=2023, color='black', linestyle=':', alpha=0.7, label='Projection')
- ax4.set_title('Projection to 2030', fontsize=14, fontweight='bold')
- ax4.set_xlabel('Year')
- ax4.set_ylabel('Normalized Index')
- ax4.legend()
- ax4.grid(True, alpha=0.3)
-
- plt.tight_layout()
- plt.savefig('../images/climate_impact_summary.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: climate_impact_summary.png")
-
-def plot_ufpr_migration_analysis():
- """
- UFPR-specific migration analysis chart.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
- fig.suptitle('UFPR Migration Analysis: South American Bees Moving South',
- fontsize=16, fontweight='bold')
-
- # 1. Species distribution by region
- regions = ['North', 'Northeast', 'Center-West', 'Southeast', 'South']
- current_species = [12, 8, 15, 18, 22] # Current distribution
- projected_species = [8, 5, 12, 20, 28] # Projected 2040
-
- x = np.arange(len(regions))
- width = 0.35
-
- bars1 = ax1.bar(x - width/2, current_species, width, label='Current (2020)', color='lightblue', alpha=0.7)
- bars2 = ax1.bar(x + width/2, projected_species, width, label='Projected (2040)', color='darkblue', alpha=0.7)
-
- ax1.set_title('Species Distribution by Region')
- ax1.set_xlabel('Brazilian Regions')
- ax1.set_ylabel('Number of Species')
- ax1.set_xticks(x)
- ax1.set_xticklabels(regions)
- ax1.legend()
- ax1.grid(True, alpha=0.3)
-
- # Add values on bars
- for bars in [bars1, bars2]:
- for bar in bars:
- height = bar.get_height()
- ax1.text(bar.get_x() + bar.get_width()/2., height + 0.5,
- f'{int(height)}', ha='center', va='bottom')
-
- # 2. Migration direction percentages
- directions = ['South', 'Southwest', 'Southeast', 'No Change', 'North']
- percentages = [45, 40, 10, 3, 2]
- colors_dir = ['darkgreen', 'green', 'lightgreen', 'gray', 'red']
-
- bars = ax2.bar(directions, percentages, color=colors_dir, alpha=0.7)
- ax2.set_title('Migration Direction Distribution')
- ax2.set_xlabel('Direction')
- ax2.set_ylabel('Percentage of Species (%)')
- ax2.set_ylim(0, 50)
-
- # Add percentage values
- for bar, value in zip(bars, percentages):
- height = bar.get_height()
- ax2.text(bar.get_x() + bar.get_width()/2., height + (1 if height > 0 else -3),
- f'{value}%', ha='center', va='bottom' if height > 0 else 'top')
-
- # 3. Impact factors
- factors = ['Temperature', 'Precipitation', 'Land Use', 'Monocultures']
- impact_scores = [9, 7, 8, 6] # Scale 1-10
- ax3.barh(factors, impact_scores, color='orange', alpha=0.7)
- ax3.set_title('Migration Impact Factors')
- ax3.set_xlabel('Impact Intensity (1-10)')
- ax3.set_xlim(0, 10)
-
- # 4. Temporal migration projection
- projection_years = np.arange(2024, 2051, 5)
- migration_intensity = np.array([0, 15, 35, 60, 80, 100]) # Percentage of species migrating
- ax4.plot(projection_years, migration_intensity, 'bo-', linewidth=3, markersize=6)
- ax4.fill_between(projection_years, migration_intensity, alpha=0.3, color='blue')
- ax4.set_title('Southward Migration Intensity Projection')
- ax4.set_xlabel('Year')
- ax4.set_ylabel('Migrating Species (%)')
- ax4.grid(True, alpha=0.3)
-
- plt.tight_layout()
- plt.savefig('../images/ufpr_migration_analysis.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: ufpr_migration_analysis.png")
-
-def plot_comparative_temperature_analysis():
- """
- Comparative temperature analysis between local data and scientific studies.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
- fig.suptitle('Temperature Trends Comparison: Local Data vs Scientific Studies',
- fontsize=16, fontweight='bold')
-
- # 1. Temperature projections by region
- regions = ['North\n(Local Data)', 'Center\n(Local Data)', 'South\n(Local Data)',
- 'UFPR\nProjection', 'Aegean\nProjection']
- temp_increases = [3.2, 2.8, 2.1, 2.5, 3.5] # °C
- colors = ['red', 'orange', 'yellow', 'blue', 'purple']
-
- bars = ax1.bar(regions, temp_increases, color=colors, alpha=0.7)
- ax1.set_title('Projected Temperature Increase by Region')
- ax1.set_ylabel('Temperature Increase (°C)')
- ax1.set_ylim(0, 4)
-
- # Add values on bars
- for bar, value in zip(bars, temp_increases):
- height = bar.get_height()
- ax1.text(bar.get_x() + bar.get_width()/2., height + 0.05,
- f'{value}°C', ha='center', va='bottom')
-
- # 2. Timeline of observed changes
- years = np.array([1960, 1990, 2000, 2010, 2020, 2024, 2030, 2040, 2050])
-
- # Bombus bellicosus (UFPR)
- bombus_presence = np.array([100, 80, 60, 40, 20, 0, 0, 0, 0]) # % presence
- ax2.plot(years, bombus_presence, 'ro-', linewidth=3, markersize=6, label='Bombus bellicosus (UFPR)')
-
- # General bee abundance (local data)
- general_abundance = np.array([100, 95, 90, 85, 75, 70, 60, 50, 40]) # % abundance
- ax2.plot(years, general_abundance, 'bo-', linewidth=3, markersize=6, label='General Abundance (Local)')
-
- ax2.axvline(x=2024, color='black', linestyle='--', alpha=0.7, label='Present')
- ax2.set_title('Species Abundance Timeline')
- ax2.set_xlabel('Year')
- ax2.set_ylabel('Relative Abundance (%)')
- ax2.legend()
- ax2.grid(True, alpha=0.3)
-
- # 3. Critical temperature thresholds
- species_groups = ['Eusocial\nSpecies', 'Solitary\nSpecies', 'Generalist\nSpecies', 'Specialist\nSpecies']
- critical_temps = [2.0, 2.5, 1.8, 3.0] # Critical temperature thresholds
- colors_temp = ['green', 'blue', 'orange', 'red']
-
- bars = ax3.bar(species_groups, critical_temps, color=colors_temp, alpha=0.7)
- ax3.axhline(y=2.5, color='red', linestyle='--', alpha=0.8, label='Global Average Threshold')
- ax3.set_title('Critical Temperature Thresholds by Functional Group')
- ax3.set_ylabel('Critical Temperature Increase (°C)')
- ax3.legend()
- ax3.grid(True, alpha=0.3)
-
- # Add values on bars
- for bar, value in zip(bars, critical_temps):
- height = bar.get_height()
- ax3.text(bar.get_x() + bar.get_width()/2., height + 0.05,
- f'{value}°C', ha='center', va='bottom')
-
- # 4. Regional vulnerability index
- regions_vuln = ['Amazon', 'Cerrado', 'Atlantic Forest', 'Caatinga', 'Pampa']
- vulnerability = [7.5, 8.2, 9.1, 6.8, 8.8] # Vulnerability index (1-10)
- colors_vuln = ['darkgreen', 'brown', 'green', 'yellow', 'lightgreen']
-
- bars = ax4.barh(regions_vuln, vulnerability, color=colors_vuln, alpha=0.7)
- ax4.set_title('Climate Vulnerability Index by Biome')
- ax4.set_xlabel('Vulnerability Index (1-10)')
- ax4.set_xlim(0, 10)
-
- # Add values on bars
- for i, (bar, value) in enumerate(zip(bars, vulnerability)):
- ax4.text(value + 0.1, bar.get_y() + bar.get_height()/2.,
- f'{value}', ha='left', va='center')
-
- plt.tight_layout()
- plt.savefig('../images/comparative_temperature_analysis.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: comparative_temperature_analysis.png")
-
-def plot_migration_patterns_synthesis():
- """
- Synthesis of migration patterns integrating multiple sources.
- """
- fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
- fig.suptitle('Bee Migration Patterns: Scientific Evidence Synthesis',
- fontsize=16, fontweight='bold')
-
- # 1. Migration direction by study
- studies = ['UFPR\n(18 spp)', 'Dryad\n(Neotropical)', 'North America\n(Bombus)', 'Aegean\n(Islands)']
- south_migration = [85, 90, 70, 60] # % species migrating south
- colors = ['green', 'blue', 'orange', 'purple']
-
- bars = ax1.bar(studies, south_migration, color=colors, alpha=0.7)
- ax1.set_title('Percentage of Species with Southward Migration')
- ax1.set_ylabel('Southward Migration (%)')
- ax1.set_ylim(0, 100)
-
- for bar, value in zip(bars, south_migration):
- height = bar.get_height()
- ax1.text(bar.get_x() + bar.get_width()/2., height + 1,
- f'{value}%', ha='center', va='bottom')
-
- # 2. Projected migration velocity
- years = np.arange(2024, 2051, 3)
-
- # Different migration scenarios (adjusted for 9 points)
- conservative = np.array([0, 10, 25, 40, 55, 70, 80, 85, 90]) # % migrated species
- moderate = np.array([0, 15, 35, 55, 70, 80, 88, 92, 95])
- accelerated = np.array([0, 20, 45, 65, 80, 90, 95, 98, 100])
-
- ax2.plot(years, conservative, 'g-', linewidth=2, label='Conservative Scenario', marker='o')
- ax2.plot(years, moderate, 'b-', linewidth=2, label='Moderate Scenario', marker='s')
- ax2.plot(years, accelerated, 'r-', linewidth=2, label='Accelerated Scenario', marker='^')
-
- ax2.fill_between(years, conservative, accelerated, alpha=0.2, color='gray')
- ax2.set_title('Migration Velocity Projections')
- ax2.set_xlabel('Year')
- ax2.set_ylabel('Cumulative Migration (%)')
- ax2.legend()
- ax2.grid(True, alpha=0.3)
-
- # 3. Functional group responses
- groups = ['Eusocial\nBees', 'Solitary\nBees', 'Generalist\nPollinators', 'Specialist\nPollinators']
- migration_rates = [75, 85, 65, 95] # % showing migration
- adaptation_capacity = [60, 40, 80, 20] # % with adaptation capacity
-
- x = np.arange(len(groups))
- width = 0.35
-
- bars1 = ax3.bar(x - width/2, migration_rates, width, label='Migration Rate (%)', color='red', alpha=0.7)
- bars2 = ax3.bar(x + width/2, adaptation_capacity, width, label='Adaptation Capacity (%)', color='blue', alpha=0.7)
-
- ax3.set_title('Functional Group Response to Climate Change')
- ax3.set_xlabel('Functional Groups')
- ax3.set_ylabel('Percentage (%)')
- ax3.set_xticks(x)
- ax3.set_xticklabels(groups)
- ax3.legend()
- ax3.grid(True, alpha=0.3)
-
- # 4. Conservation priority areas
- areas = ['Southern\nBrazilian\nFields', 'Atlantic\nForest\nRemnants', 'Cerrado\nTransition', 'Coastal\nPlains', 'Mountain\nCorridors']
- priority_scores = [9.5, 8.8, 8.2, 7.5, 9.0] # Priority score (1-10)
- species_richness = [45, 38, 42, 28, 35] # Number of species
-
- # Create dual y-axis
- ax4_twin = ax4.twinx()
-
- bars = ax4.bar(areas, priority_scores, color='darkgreen', alpha=0.7, label='Priority Score')
- line = ax4_twin.plot(areas, species_richness, 'ro-', linewidth=2, markersize=6, label='Species Richness')
-
- ax4.set_title('Conservation Priority Areas')
- ax4.set_ylabel('Priority Score (1-10)', color='darkgreen')
- ax4_twin.set_ylabel('Species Richness', color='red')
- ax4.tick_params(axis='y', labelcolor='darkgreen')
- ax4_twin.tick_params(axis='y', labelcolor='red')
-
- # Rotate x-axis labels
- ax4.tick_params(axis='x', rotation=45)
-
- plt.tight_layout()
- plt.savefig('../images/migration_patterns_synthesis.png', dpi=300, bbox_inches='tight')
- plt.close()
- print("Chart saved: migration_patterns_synthesis.png")
-
-def main():
- """
- Main function that runs all analyses and generates charts.
- """
- print("Starting chart generation for bee analysis...\n")
-
- # Generate simulated data
- print("Generating simulated data...")
- df = generate_sample_data()
- geo_df = generate_geographic_data()
-
- print(f"Data generated: {len(df)} years of temporal data")
- print(f"Geographic data: {len(geo_df)} distribution points\n")
-
- # Generate charts
- print("Generating charts...")
-
- # Basic analysis charts
- plot_temperature_migration_trend(df)
- plot_correlation_analysis(df)
- plot_species_decline(df)
- plot_geographic_distribution(geo_df)
- plot_climate_impact_summary(df)
-
- # Scientific paper-specific charts
- plot_ufpr_migration_analysis()
- plot_comparative_temperature_analysis()
- plot_migration_patterns_synthesis()
-
- print("\nAll charts generated successfully!")
- print("Files saved in 'images/' folder")
- print("\nGenerated charts:")
- print(" 1. temperature_migration_trend.png - Temperature and migration trends")
- print(" 2. correlation_analysis.png - Correlation analysis")
- print(" 3. species_honey_decline.png - Species and production decline")
- print(" 4. geographic_distribution.png - Geographic distribution")
- print(" 5. climate_impact_summary.png - Climate impact summary")
- print(" 6. ufpr_migration_analysis.png - UFPR migration analysis")
- print(" 7. comparative_temperature_analysis.png - Temperature comparison")
- print(" 8. migration_patterns_synthesis.png - Migration patterns synthesis")
-
- # Save data for future use
- df.to_csv('../data/bee_climate_data.csv', index=False)
- geo_df.to_csv('../data/bee_geographic_data.csv', index=False)
- print("\nData saved in 'data/' for future analyses")
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..29e9484
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,35 @@
+import pandas as pd
+import pytest
+
+from bee_migration.climate.observations import Observations
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.climate.simulation import ClimateSimulation
+
+
+@pytest.fixture(scope="session")
+def observations() -> Observations:
+ simulation = ClimateSimulation(BRAZILIAN_REGIONS, StudyPeriod(2000, 2023))
+ return simulation.observations(seed=42)
+
+
+def region_table(region: str, **columns: list[float]) -> pd.DataFrame:
+ """Build a raw single-region table, filling unspecified measurements."""
+ length = len(next(iter(columns.values())))
+ defaults = {
+ "temperature": [20.0 + step for step in range(length)],
+ "precipitation": [1000.0 - 10 * step for step in range(length)],
+ "bee_abundance": [80.0 - step for step in range(length)],
+ "species_diversity": [40.0 - step for step in range(length)],
+ "honey_production": [25.0 - step for step in range(length)],
+ }
+ return pd.DataFrame(
+ {
+ "year": range(2000, 2000 + length),
+ "region": region,
+ "latitude": -10.0,
+ "longitude": -50.0,
+ **defaults,
+ **columns,
+ }
+ )
diff --git a/tests/test_analysis.py b/tests/test_analysis.py
new file mode 100644
index 0000000..862252b
--- /dev/null
+++ b/tests/test_analysis.py
@@ -0,0 +1,111 @@
+import numpy as np
+import pandas as pd
+import pytest
+
+from bee_migration.analysis.correlation import CorrelationPair, Correlations
+from bee_migration.analysis.geographic import GeographicPatterns
+from bee_migration.analysis.temporal import TemporalPatterns
+from bee_migration.climate.observations import Observations
+from tests.conftest import region_table
+
+
+def test_trends_show_warming_and_bee_decline_in_every_region(observations):
+ trends = TemporalPatterns(observations).trends()
+
+ assert (trends["temperature"] > 0).all()
+ assert (trends["bee_abundance"] < 0).all()
+ assert list(trends.index) == observations.region_names()
+
+
+def test_trend_is_the_yearly_slope():
+ table = region_table("North", temperature=[10.0, 10.5, 11.0, 11.5])
+
+ trends = TemporalPatterns(Observations(table)).trends()
+
+ assert trends.loc["North", "temperature"] == pytest.approx(0.5)
+
+
+def test_change_points_flag_the_year_of_an_abrupt_shift():
+ steady = [20.0] * 20
+ jump = steady[:12] + [value + 9.0 for value in steady[12:]]
+ table = region_table("North", temperature=jump)
+
+ changes = TemporalPatterns(Observations(table)).change_points()
+
+ assert 2012 in changes.loc["North", "temperature"]
+
+
+def test_correlation_matrix_is_symmetric_with_unit_diagonal(observations):
+ matrix = Correlations(observations).matrix()
+
+ assert np.allclose(matrix, matrix.T)
+ assert np.allclose(np.diag(matrix), 1.0)
+
+
+def test_notable_pairs_are_ordered_from_strongest(observations):
+ pairs = Correlations(observations).notable_pairs(threshold=0.3)
+
+ magnitudes = [abs(pair.coefficient) for pair in pairs]
+
+ assert magnitudes == sorted(magnitudes, reverse=True)
+ assert min(magnitudes) >= 0.3
+
+
+def test_notable_pairs_never_pair_a_variable_with_itself(observations):
+ pairs = Correlations(observations).notable_pairs(threshold=0.0)
+
+ assert all(pair.variables[0] != pair.variables[1] for pair in pairs)
+
+
+@pytest.mark.parametrize(
+ ("coefficient", "strength"),
+ [(0.9, "strong"), (-0.75, "strong"), (0.4, "moderate"), (0.1, "weak")],
+)
+def test_correlation_pair_names_its_strength(coefficient, strength):
+ pair = CorrelationPair(("temperature", "bee_abundance"), coefficient)
+
+ assert pair.strength() == strength
+
+
+def test_lagged_correlation_pairs_temperature_with_later_abundance():
+ temperature = [20.0, 23.0, 21.0, 26.0, 22.0, 27.0, 24.0, 29.0]
+ delayed = [50.0] + [100.0 - value for value in temperature[:-1]]
+ table = region_table("North", temperature=temperature, bee_abundance=delayed)
+
+ lagged = Correlations(Observations(table)).lagged(lags=(0, 1))
+
+ assert lagged.loc["North", 1] == pytest.approx(-1.0)
+ assert abs(lagged.loc["North", 0]) < 0.9
+
+
+def test_regional_correlations_cover_every_region(observations):
+ regional = Correlations(observations).by_region()
+
+ assert sorted(regional) == observations.region_names()
+
+
+def test_regional_summary_reports_spread_per_measurement(observations):
+ summary = GeographicPatterns(observations).summary()
+
+ assert (
+ summary.loc["North", ("temperature", "max")]
+ >= summary.loc["North", ("temperature", "min")]
+ )
+
+
+def test_latitudinal_gradient_covers_every_measurement(observations):
+ gradients = GeographicPatterns(observations).latitudinal_gradients()
+
+ assert gradients.between(-1.0, 1.0).all()
+ assert "bee_abundance" in gradients.index
+
+
+def test_hotspots_rank_regions_by_combined_change():
+ calm = region_table("Calm", temperature=[20.0, 20.1], bee_abundance=[80.0, 79.0])
+ harsh = region_table("Harsh", temperature=[20.0, 24.0], bee_abundance=[80.0, 40.0])
+ observations = Observations(pd.concat([calm, harsh], ignore_index=True))
+
+ hotspots = GeographicPatterns(observations).hotspots()
+
+ assert hotspots.index[0] == "Harsh"
+ assert hotspots.loc["Harsh", "bee_abundance_change"] == pytest.approx(40.0)
diff --git a/tests/test_climate.py b/tests/test_climate.py
new file mode 100644
index 0000000..2e08846
--- /dev/null
+++ b/tests/test_climate.py
@@ -0,0 +1,166 @@
+import numpy as np
+import pandas as pd
+import pytest
+
+from bee_migration.climate.continental import continental_series, distribution_shift
+from bee_migration.climate.observations import HEALTH_WEIGHTS, Observations
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.climate.simulation import ClimateSimulation
+from bee_migration.climate.trend import Drift, Trend
+from tests.conftest import region_table
+
+
+def test_study_period_lists_every_year_inclusive():
+ years = StudyPeriod(2000, 2003).years()
+
+ assert years.tolist() == [2000, 2001, 2002, 2003]
+
+
+def test_study_period_rejects_inverted_bounds():
+ with pytest.raises(ValueError, match="before"):
+ StudyPeriod(2010, 2000)
+
+
+def test_trend_without_noise_follows_its_drift():
+ trend = Trend(Drift(start=10.0, total_change=4.0), spread=0.0)
+
+ sample = trend.sample(5, np.random.default_rng(0))
+
+ assert sample.tolist() == [10.0, 11.0, 12.0, 13.0, 14.0]
+
+
+def test_trend_never_falls_below_its_floor():
+ trend = Trend(Drift(start=5.0, total_change=-50.0), spread=3.0, floor=2.0)
+
+ sample = trend.sample(100, np.random.default_rng(0))
+
+ assert sample.min() >= 2.0
+
+
+def test_simulation_yields_one_row_per_region_and_year(observations):
+ assert len(observations) == len(BRAZILIAN_REGIONS) * 24
+ assert observations.region_names() == sorted(
+ profile.name for profile in BRAZILIAN_REGIONS
+ )
+
+
+def test_simulation_is_reproducible_for_the_same_seed():
+ simulation = ClimateSimulation(BRAZILIAN_REGIONS, StudyPeriod(2000, 2005))
+
+ first = simulation.observations(seed=7).table()
+ second = simulation.observations(seed=7).table()
+ other = simulation.observations(seed=8).table()
+
+ pd.testing.assert_frame_equal(first, second)
+ assert not first["temperature"].equals(other["temperature"])
+
+
+def test_observations_reject_tables_missing_measurements():
+ incomplete = region_table("North", temperature=[20.0, 21.0]).drop(
+ columns="precipitation"
+ )
+
+ with pytest.raises(ValueError, match="precipitation"):
+ Observations(incomplete)
+
+
+def test_observations_reject_empty_tables():
+ empty = region_table("North", temperature=[20.0]).iloc[0:0]
+
+ with pytest.raises(ValueError, match="empty"):
+ Observations(empty)
+
+
+def test_observations_reject_non_numeric_measurements():
+ table = region_table("North", temperature=[20.0, 21.0]).astype({"temperature": str})
+ table.loc[0, "temperature"] = "warm"
+
+ with pytest.raises(ValueError, match="non-numeric"):
+ Observations(table)
+
+
+def test_observations_reject_missing_values():
+ table = region_table("North", temperature=[20.0, float("nan"), 22.0])
+
+ with pytest.raises(ValueError, match="missing values"):
+ Observations(table)
+
+
+def test_observations_reject_a_year_repeated_within_a_region():
+ table = region_table("North", temperature=[20.0, 21.0])
+
+ with pytest.raises(ValueError, match="repeat a year"):
+ Observations(pd.concat([table, table], ignore_index=True))
+
+
+def test_observations_reject_gaps_between_years():
+ table = region_table("North", temperature=[20.0, 21.0, 22.0, 23.0]).drop(index=1)
+
+ with pytest.raises(ValueError, match="skip years"):
+ Observations(table)
+
+
+def test_anomalies_are_centred_within_each_region(observations):
+ table = observations.table()
+
+ centres = table.groupby("region")["temperature_anomaly"].mean()
+
+ assert np.allclose(centres, 0.0)
+
+
+def test_health_index_weighs_abundance_diversity_and_honey():
+ table = region_table(
+ "North",
+ bee_abundance=[100.0, 50.0],
+ species_diversity=[10.0, 20.0],
+ honey_production=[30.0, 40.0],
+ )
+
+ health = Observations(table).table()["bee_health_index"]
+
+ expected = (
+ HEALTH_WEIGHTS["bee_abundance"] * 100.0
+ + HEALTH_WEIGHTS["species_diversity"] * 10.0
+ + HEALTH_WEIGHTS["honey_production"] * 30.0
+ )
+ assert health.iloc[0] == pytest.approx(expected)
+
+
+def test_observations_survive_a_csv_round_trip(observations, tmp_path):
+ path = tmp_path / "nested" / "observations.csv"
+
+ observations.save(path)
+ restored = Observations.from_csv(path)
+
+ pd.testing.assert_frame_equal(restored.table(), observations.table())
+
+
+def test_table_is_a_defensive_copy(observations):
+ table = observations.table()
+ table["temperature"] = 0.0
+
+ assert observations.table()["temperature"].mean() > 0.0
+
+
+def test_continental_series_drifts_south_while_warming():
+ series = continental_series(StudyPeriod(2000, 2023), seed=42)
+
+ assert series["temperature"].iloc[-1] > series["temperature"].iloc[0]
+ assert series["latitude"].iloc[-1] < series["latitude"].iloc[0]
+ assert list(series.columns) == [
+ "year",
+ "temperature",
+ "latitude",
+ "species_count",
+ "honey_production",
+ ]
+
+
+def test_distribution_shift_projects_a_more_southern_range():
+ points = distribution_shift(seed=42)
+
+ latitude = points.groupby("period")["latitude"].mean()
+
+ assert len(points) == 200
+ assert latitude["Projected (2040-2050)"] < latitude["Current (2020-2023)"]
diff --git a/tests/test_outputs.py b/tests/test_outputs.py
new file mode 100644
index 0000000..83deb3d
--- /dev/null
+++ b/tests/test_outputs.py
@@ -0,0 +1,96 @@
+import pytest
+from sklearn.linear_model import LinearRegression
+
+from bee_migration import cli
+from bee_migration.charts.gallery import ChartGallery
+from bee_migration.climate.continental import continental_series, distribution_shift
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.prediction.catalog import Candidate
+from bee_migration.prediction.training import MigrationPredictor
+from bee_migration.reports.analysis_report import AnalysisReport
+from bee_migration.reports.prediction_report import PredictionReport
+
+FAST_CATALOG = (Candidate("Linear Regression", LinearRegression(), {}),)
+
+EXPECTED_CHARTS = [
+ "climate_impact_summary.png",
+ "comparative_temperature_analysis.png",
+ "correlation_analysis.png",
+ "geographic_distribution.png",
+ "migration_patterns_synthesis.png",
+ "species_honey_decline.png",
+ "temperature_migration_trend.png",
+ "ufpr_migration_analysis.png",
+]
+
+
+def test_analysis_report_summarises_every_region(observations):
+ text = AnalysisReport(observations).text()
+
+ assert "2000 - 2023" in text
+ assert all(region in text for region in observations.region_names())
+ assert "Warming in 5 of 5 regions" in text
+
+
+def test_prediction_report_names_the_best_model(observations):
+ leaderboard = MigrationPredictor(FAST_CATALOG, holdout_years=4).train(observations)
+
+ text = PredictionReport(leaderboard).text()
+
+ assert "Best model: Linear Regression" in text
+ assert "Test R2" in text
+
+
+def test_gallery_renders_every_chart(tmp_path):
+ series = continental_series(StudyPeriod(2000, 2023), seed=42)
+ gallery = ChartGallery(tmp_path / "images", resolution=40)
+
+ rendered = gallery.render(series, distribution_shift(seed=42))
+
+ assert sorted(path.name for path in rendered) == EXPECTED_CHARTS
+ assert all(path.stat().st_size > 0 for path in rendered)
+
+
+def test_analyze_command_writes_report_and_processed_data(tmp_path):
+ exit_code = cli.main(["analyze", "--root", str(tmp_path)])
+
+ assert exit_code == 0
+ assert (tmp_path / "results" / "analysis_summary_report.txt").exists()
+ assert (tmp_path / "data" / "processed" / "bee_climate_processed.csv").exists()
+ assert (
+ tmp_path / "results" / "correlation_analysis_correlation_matrix.csv"
+ ).exists()
+
+
+def test_analyze_command_accepts_previously_saved_observations(observations, tmp_path):
+ source = tmp_path / "input.csv"
+ observations.save(source)
+
+ exit_code = cli.main(["analyze", "--root", str(tmp_path), "--data", str(source)])
+
+ assert exit_code == 0
+
+
+def test_predict_command_writes_metrics_and_models(tmp_path, monkeypatch):
+ monkeypatch.setattr(cli, "DEFAULT_CANDIDATES", FAST_CATALOG)
+
+ exit_code = cli.main(["predict", "--root", str(tmp_path)])
+
+ assert exit_code == 0
+ assert (tmp_path / "results" / "model_metrics.csv").exists()
+ assert (tmp_path / "results" / "feature_importance.csv").exists()
+ assert (tmp_path / "results" / "models" / "linear_regression_model.pkl").exists()
+
+
+def test_missing_data_file_is_reported_without_a_traceback(tmp_path, caplog):
+ exit_code = cli.main(
+ ["analyze", "--root", str(tmp_path), "--data", str(tmp_path / "absent.csv")]
+ )
+
+ assert exit_code == 1
+ assert "absent.csv" in caplog.text
+
+
+def test_unknown_command_is_rejected():
+ with pytest.raises(SystemExit):
+ cli.main(["swarm"])
diff --git a/tests/test_prediction.py b/tests/test_prediction.py
new file mode 100644
index 0000000..5916b93
--- /dev/null
+++ b/tests/test_prediction.py
@@ -0,0 +1,137 @@
+import joblib
+import numpy as np
+import pytest
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.linear_model import LinearRegression
+
+from bee_migration.climate.observations import Observations
+from bee_migration.prediction.catalog import DEFAULT_CANDIDATES, Candidate
+from bee_migration.prediction.features import FeatureTable
+from bee_migration.prediction.scores import RegressionScore
+from bee_migration.prediction.training import MigrationPredictor
+from tests.conftest import region_table
+
+SMALL_CATALOG = (
+ Candidate("Linear Regression", LinearRegression(), {}),
+ Candidate(
+ "Random Forest",
+ RandomForestRegressor(n_estimators=20, random_state=42),
+ {"max_depth": [2, 4]},
+ ),
+)
+
+
+@pytest.fixture(scope="module")
+def leaderboard(observations):
+ return MigrationPredictor(SMALL_CATALOG, holdout_years=4).train(observations)
+
+
+def test_features_exclude_anything_derived_from_the_current_target(observations):
+ names = FeatureTable(observations).names()
+
+ assert "bee_abundance" not in names
+ assert "bee_abundance_change" not in names
+ assert "bee_health_index" not in names
+ assert "bee_abundance_previous_year" in names
+
+
+def test_features_drop_the_first_year_which_has_no_previous_year(observations):
+ split = FeatureTable(observations).split(holdout_years=4)
+
+ rows = len(split.train.target) + len(split.test.target)
+
+ assert rows == len(observations) - len(observations.region_names())
+
+
+def test_every_training_year_precedes_every_test_year(observations):
+ table = FeatureTable(observations)
+ year = table.names().index("year_normalized")
+
+ split = table.split(holdout_years=4)
+
+ assert split.train.features[:, year].max() < split.test.features[:, year].min()
+
+
+def test_previous_year_feature_carries_last_years_target():
+ abundance = [80.0, 70.0, 65.0, 50.0, 45.0, 30.0]
+ table = FeatureTable(Observations(region_table("North", bee_abundance=abundance)))
+ previous = table.names().index("bee_abundance_previous_year")
+
+ split = table.split(holdout_years=2)
+
+ assert split.train.features[:, previous].tolist() == [80.0, 70.0, 65.0]
+ assert split.train.target.tolist() == [70.0, 65.0, 50.0]
+ assert split.test.features[:, previous].tolist() == [50.0, 45.0]
+
+
+@pytest.mark.parametrize("target", ["temperature", "precipitation", "wingspan"])
+def test_feature_table_only_predicts_bee_measurements(observations, target):
+ with pytest.raises(ValueError, match="target"):
+ FeatureTable(observations, target=target)
+
+
+def test_split_holds_out_the_most_recent_years(observations):
+ split = FeatureTable(observations).split(holdout_years=4)
+
+ assert len(split.test.target) == 4 * len(observations.region_names())
+ assert split.train.features.shape[1] == split.test.features.shape[1]
+
+
+@pytest.mark.parametrize("holdout_years", [0, 23, 99])
+def test_split_rejects_holdouts_leaving_nothing_to_train_or_test(
+ observations, holdout_years
+):
+ with pytest.raises(ValueError, match="holdout"):
+ FeatureTable(observations).split(holdout_years=holdout_years)
+
+
+def test_perfect_predictions_score_without_error():
+ actual = np.array([10.0, 20.0, 30.0])
+
+ score = RegressionScore.of(actual, actual)
+
+ assert score.rmse == 0.0
+ assert score.mae == 0.0
+ assert score.r2 == 1.0
+ assert score.mape == 0.0
+
+
+def test_score_measures_average_miss():
+ score = RegressionScore.of(np.array([10.0, 20.0]), np.array([12.0, 18.0]))
+
+ assert score.mae == pytest.approx(2.0)
+ assert score.rmse == pytest.approx(2.0)
+ assert score.mape == pytest.approx(15.0)
+
+
+def test_leaderboard_ranks_every_candidate(leaderboard):
+ metrics = leaderboard.metrics()
+
+ assert sorted(metrics.index) == ["Linear Regression", "Random Forest"]
+ assert leaderboard.best().name == metrics["test_r2"].idxmax()
+
+
+def test_best_model_generalises_to_unseen_years(leaderboard):
+ assert leaderboard.best().evaluation.test.r2 > 0.0
+
+
+def test_importances_cover_every_feature_per_model(leaderboard, observations):
+ importances = leaderboard.importances()
+
+ assert list(importances.index) == FeatureTable(observations).names()
+ assert list(importances.columns) == ["Linear Regression", "Random Forest"]
+
+
+def test_saved_models_can_be_reloaded_to_predict(leaderboard, observations, tmp_path):
+ leaderboard.save(tmp_path / "models")
+ sample = FeatureTable(observations).split(holdout_years=4).test
+
+ restored = joblib.load(tmp_path / "models" / "linear_regression_model.pkl")
+
+ assert restored.predict(sample.features).shape == sample.target.shape
+
+
+def test_default_catalog_offers_the_four_documented_models():
+ names = [candidate.name for candidate in DEFAULT_CANDIDATES]
+
+ assert names == ["Linear Regression", "Random Forest", "Gradient Boosting", "SVR"]
diff --git a/tests/test_sources.py b/tests/test_sources.py
new file mode 100644
index 0000000..b60b773
--- /dev/null
+++ b/tests/test_sources.py
@@ -0,0 +1,145 @@
+import json
+from urllib.parse import parse_qs, urlparse
+
+import pytest
+
+from bee_migration.climate.period import StudyPeriod
+from bee_migration.climate.region import BRAZILIAN_REGIONS
+from bee_migration.sources.gbif import (
+ LatitudeBand,
+ latitude_bands,
+ record_centroids,
+ yearly_counts,
+)
+from bee_migration.sources.open_meteo import archive_url, yearly_climate
+from bee_migration.web.field import field_report, field_requests
+
+PERIOD = StudyPeriod(2000, 2001)
+
+
+def location(temperatures: list[float | None]) -> dict:
+ days = ["2000-12-30", "2000-12-31", "2001-01-01", "2001-01-02"]
+ return {"daily": {"time": days, "temperature_2m_mean": temperatures}}
+
+
+def facets(counts: dict[str, int]) -> dict:
+ listed = [{"name": year, "count": count} for year, count in counts.items()]
+ return {
+ "count": sum(counts.values()),
+ "facets": [{"field": "YEAR", "counts": listed}],
+ }
+
+
+def test_archive_url_asks_for_every_region_over_the_whole_period():
+ query = parse_qs(urlparse(archive_url(BRAZILIAN_REGIONS, PERIOD)).query)
+
+ assert query["latitude"] == ["-3.0,-9.0,-15.0,-20.0,-27.0"]
+ assert query["start_date"] == ["2000-01-01"]
+ assert query["end_date"] == ["2001-12-31"]
+ assert query["daily"] == ["temperature_2m_mean"]
+
+
+def test_yearly_climate_averages_the_daily_temperatures_of_each_year():
+ climate = yearly_climate([location([20.0, 22.0, 30.0, 32.0])], ["North"])
+
+ assert climate.to_dict("records") == [
+ {"year": 2000, "region": "North", "temperature": 21.0},
+ {"year": 2001, "region": "North", "temperature": 31.0},
+ ]
+
+
+def test_yearly_climate_accepts_the_single_location_shape():
+ climate = yearly_climate(location([20.0, 22.0, 30.0, 32.0]), ["South"])
+
+ assert climate["region"].unique().tolist() == ["South"]
+
+
+def test_yearly_climate_ignores_days_without_a_reading():
+ climate = yearly_climate([location([20.0, None, 30.0, 32.0])], ["North"])
+
+ assert climate.loc[0, "temperature"] == 20.0
+
+
+def test_yearly_climate_rejects_a_mismatched_number_of_locations():
+ with pytest.raises(ValueError, match="locations"):
+ yearly_climate([location([1.0] * 4)], ["North", "South"])
+
+
+def test_yearly_climate_rejects_an_error_answer():
+ with pytest.raises(ValueError, match="Open-Meteo"):
+ yearly_climate({"error": True, "reason": "Too many requests"}, ["North"])
+
+
+def test_latitude_bands_tile_the_continent_without_gaps():
+ bands = latitude_bands(south=-10.0, north=5.0, height=5.0)
+
+ assert bands == [
+ LatitudeBand(-10.0, -5.0),
+ LatitudeBand(-5.0, 0.0),
+ LatitudeBand(0.0, 5.0),
+ ]
+
+
+def test_band_url_filters_bees_by_latitude_and_year():
+ query = parse_qs(urlparse(LatitudeBand(-30.0, -25.0).url(PERIOD)).query)
+
+ assert query["decimalLatitude"] == ["-30.0,-25.0"]
+ assert query["year"] == ["2000,2001"]
+ assert query["facet"] == ["year"]
+ assert query["limit"] == ["0"]
+
+
+def test_yearly_counts_reads_the_year_facet():
+ assert yearly_counts(facets({"2000": 12, "2001": 30})) == {2000: 12, 2001: 30}
+
+
+def test_yearly_counts_rejects_answers_without_the_year_facet():
+ with pytest.raises(ValueError, match="GBIF"):
+ yearly_counts({"count": 3, "facets": []})
+
+
+def test_centroid_is_the_record_weighted_mean_latitude():
+ counts = {
+ LatitudeBand(-30.0, -20.0): {2000: 10, 2001: 30},
+ LatitudeBand(-10.0, 0.0): {2000: 30, 2001: 10},
+ }
+
+ centroids = record_centroids(counts)
+
+ assert centroids.to_dict("records") == [
+ {"year": 2000, "records": 40, "mean_latitude": -10.0},
+ {"year": 2001, "records": 40, "mean_latitude": -20.0},
+ ]
+
+
+def test_centroid_skips_years_without_records():
+ counts = {LatitudeBand(-10.0, 0.0): {2001: 4}}
+
+ assert record_centroids(counts)["year"].tolist() == [2001]
+
+
+def test_field_requests_list_one_climate_url_and_one_url_per_band():
+ requests = json.loads(field_requests())
+
+ assert requests["climate"].startswith("https://archive-api.open-meteo.com/")
+ assert len(requests["bands"]) == 14
+ assert all(
+ band["url"].startswith("https://api.gbif.org/") for band in requests["bands"]
+ )
+
+
+def test_field_report_summarises_warming_and_the_drift_of_records():
+ names = [profile.name for profile in BRAZILIAN_REGIONS]
+ climate = [location([20.0, 20.0, 21.0, 21.0]) for _ in names]
+ bands = [
+ {"south": -30.0, "north": -20.0, "answer": facets({"2000": 10, "2001": 30})},
+ {"south": -10.0, "north": 0.0, "answer": facets({"2000": 30, "2001": 10})},
+ ]
+
+ report = json.loads(field_report(json.dumps({"climate": climate, "bands": bands})))
+
+ assert report["regions"] == sorted(names)
+ assert len(report["climate"]) == 2 * len(names)
+ assert report["warming_per_decade"] == pytest.approx(10.0)
+ assert report["latitude_shift_per_decade"] == pytest.approx(-100.0)
+ assert report["total_records"] == 80
diff --git a/tests/test_web.py b/tests/test_web.py
new file mode 100644
index 0000000..a3135ed
--- /dev/null
+++ b/tests/test_web.py
@@ -0,0 +1,95 @@
+import json
+
+import pytest
+
+from bee_migration.prediction.features import FeatureTable
+from bee_migration.web.study import StudyRequest, run_study, simulated_study
+
+REGIONS = 5
+
+
+@pytest.fixture(scope="module")
+def study() -> dict:
+ return simulated_study(StudyRequest(seed=42, holdout_years=4))
+
+
+def test_study_is_plain_json(study):
+ assert json.loads(json.dumps(study)) == study
+
+
+def test_study_lists_every_observation_with_its_measurements(study):
+ first = study["observations"][0]
+
+ assert len(study["observations"]) == REGIONS * 24
+ assert {"year", "region", "temperature", "bee_abundance"} <= set(first)
+
+
+def test_study_reports_one_trend_per_region(study):
+ regions = [trend["region"] for trend in study["trends"]]
+
+ assert regions == study["regions"]
+ assert all(trend["temperature"] > 0 for trend in study["trends"])
+
+
+def test_correlation_matrix_matches_its_variables(study):
+ correlations = study["correlations"]
+
+ size = len(correlations["variables"])
+
+ assert len(correlations["matrix"]) == size
+ assert all(len(row) == size for row in correlations["matrix"])
+
+
+def test_study_forecasts_every_held_out_region_and_year(study):
+ forecasts = study["forecasts"]
+ models = [model["model"] for model in study["models"]]
+
+ assert len(forecasts) == REGIONS * 4
+ assert {row["year"] for row in forecasts} == {2020, 2021, 2022, 2023}
+ assert all(name in forecasts[0] for name in models)
+ assert study["best"] in models
+
+
+def test_forecast_rows_carry_the_observed_value(study, observations):
+ table = observations.table()
+ row = study["forecasts"][0]
+
+ observed = table[
+ (table["year"] == row["year"]) & (table["region"] == row["region"])
+ ]
+
+ assert row["actual"] == pytest.approx(observed["bee_abundance"].iloc[0])
+
+
+def test_importance_ranks_features_from_most_influential(study):
+ values = [entry["importance"] for entry in study["importance"]]
+
+ assert values == sorted(values, reverse=True)
+ assert 0 < len(values) <= 8
+
+
+def test_holdout_labels_align_with_the_test_sample(observations):
+ features = FeatureTable(observations)
+
+ labels = features.holdout(4)
+ sample = features.split(4).test
+
+ assert labels["actual"].tolist() == sample.target.tolist()
+ assert list(labels.columns) == ["year", "region", "actual"]
+
+
+def test_run_study_speaks_json_both_ways():
+ answer = json.loads(run_study(json.dumps({"seed": 7, "holdout_years": 3})))
+
+ assert answer["request"] == {
+ "seed": 7,
+ "holdout_years": 3,
+ "target": "bee_abundance",
+ "thorough": False,
+ }
+ assert len(answer["forecasts"]) == REGIONS * 3
+
+
+def test_study_request_rejects_unknown_settings():
+ with pytest.raises(ValueError, match="Unknown study settings"):
+ StudyRequest.parsed(json.dumps({"seeds": 7}))
diff --git a/tests/web/format.test.mjs b/tests/web/format.test.mjs
new file mode 100644
index 0000000..eee07d8
--- /dev/null
+++ b/tests/web/format.test.mjs
@@ -0,0 +1,44 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+import {
+ formatLatitude,
+ formatMeasure,
+ formatNumber,
+ formatSigned,
+ humanize,
+} from "../../docs/scripts/format.js";
+
+test("humanize turns column names into readable labels", () => {
+ assert.equal(humanize("bee_abundance"), "Bee abundance");
+ assert.equal(humanize("temperature_previous_year"), "Temperature previous year");
+ assert.equal(humanize("region_Center-West"), "Region Center-West");
+});
+
+test("numbers keep the requested number of decimals", () => {
+ assert.equal(formatNumber(3.14159, 2), "3.14");
+ assert.equal(formatNumber(1379.4, 0), "1,379");
+});
+
+test("signed numbers always show their direction", () => {
+ assert.equal(formatSigned(1.25, 1), "+1.3");
+ assert.equal(formatSigned(-0.5, 1), "−0.5");
+ assert.equal(formatSigned(0, 1), "0.0");
+});
+
+test("measures are formatted with their unit", () => {
+ assert.equal(formatMeasure("temperature", 24.56), "24.6 °C");
+ assert.equal(formatMeasure("precipitation", 1379.4), "1,379 mm");
+ assert.equal(formatMeasure("bee_abundance", 61.12), "61.1");
+});
+
+test("latitudes name their hemisphere instead of carrying a sign", () => {
+ assert.equal(formatLatitude(-12.34), "12.3° S");
+ assert.equal(formatLatitude(4), "4.0° N");
+ assert.equal(formatLatitude(null), "–");
+});
+
+test("missing values are shown as a dash instead of NaN", () => {
+ assert.equal(formatNumber(null, 2), "–");
+ assert.equal(formatMeasure("temperature", undefined), "–");
+});
diff --git a/tests/web/scale.test.mjs b/tests/web/scale.test.mjs
new file mode 100644
index 0000000..0c86ef1
--- /dev/null
+++ b/tests/web/scale.test.mjs
@@ -0,0 +1,44 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+import { extent, linearScale, niceTicks } from "../../docs/scripts/charts/scale.js";
+
+test("extent returns the smallest and largest value", () => {
+ assert.deepEqual(extent([3, -1, 7, 2]), [-1, 7]);
+});
+
+test("linear scale maps the domain ends onto the range ends", () => {
+ const scale = linearScale([0, 10], [100, 200]);
+
+ assert.equal(scale(0), 100);
+ assert.equal(scale(5), 150);
+ assert.equal(scale(10), 200);
+});
+
+test("linear scale supports inverted ranges for vertical axes", () => {
+ const scale = linearScale([0, 4], [300, 0]);
+
+ assert.equal(scale(1), 225);
+});
+
+test("linear scale centres a domain with no spread instead of dividing by zero", () => {
+ const scale = linearScale([5, 5], [0, 100]);
+
+ assert.equal(scale(5), 0);
+});
+
+test("nice ticks use round steps that cover the whole domain", () => {
+ assert.deepEqual(niceTicks([19.8, 29.4]), [18, 20, 22, 24, 26, 28, 30]);
+});
+
+test("nice ticks stay free of floating point noise", () => {
+ assert.deepEqual(niceTicks([0.1, 0.72]), [0, 0.2, 0.4, 0.6, 0.8]);
+});
+
+test("nice ticks widen a flat domain so a lone value still gets an axis", () => {
+ const ticks = niceTicks([4, 4]);
+
+ assert.ok(ticks[0] <= 4);
+ assert.ok(ticks.at(-1) >= 4);
+ assert.ok(ticks.length >= 2);
+});
diff --git a/tests/web/url-state.test.mjs b/tests/web/url-state.test.mjs
new file mode 100644
index 0000000..76c6905
--- /dev/null
+++ b/tests/web/url-state.test.mjs
@@ -0,0 +1,61 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+
+import { DEFAULTS, readState, studyRequest, writeState } from "../../docs/scripts/url-state.js";
+
+test("an empty address yields the default study", () => {
+ assert.deepEqual(readState(""), DEFAULTS);
+});
+
+test("settings are read from the address", () => {
+ const state = readState("?seed=7&holdout=6&target=honey_production&thorough=1&measure=precipitation®ion=South");
+
+ assert.deepEqual(state, {
+ seed: 7,
+ holdout: 6,
+ target: "honey_production",
+ thorough: true,
+ measure: "precipitation",
+ region: "South",
+ });
+});
+
+test("numbers outside their limits are clamped", () => {
+ const state = readState("?seed=-5&holdout=40");
+
+ assert.equal(state.seed, 0);
+ assert.equal(state.holdout, 8);
+});
+
+test("unreadable or unknown values fall back to the defaults", () => {
+ const state = readState("?seed=abc&holdout=&target=wingspan&measure=altitude");
+
+ assert.equal(state.seed, DEFAULTS.seed);
+ assert.equal(state.holdout, DEFAULTS.holdout);
+ assert.equal(state.target, DEFAULTS.target);
+ assert.equal(state.measure, DEFAULTS.measure);
+});
+
+test("the default study writes a clean address", () => {
+ assert.equal(writeState(DEFAULTS), "");
+});
+
+test("only changed settings are written, and reading them back is lossless", () => {
+ const state = { ...DEFAULTS, seed: 99, thorough: true, region: "Center-West" };
+
+ const query = writeState(state);
+
+ assert.equal(query, "?seed=99&thorough=1®ion=Center-West");
+ assert.deepEqual(readState(query), state);
+});
+
+test("a study request carries only what the Python package expects", () => {
+ const request = studyRequest({ ...DEFAULTS, holdout: 3, measure: "precipitation" });
+
+ assert.deepEqual(request, {
+ seed: 42,
+ holdout_years: 3,
+ target: "bee_abundance",
+ thorough: false,
+ });
+});