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 + + + + + + + +
+

Field notebook · South America · 2000–2023

+

Are the bees moving south?

+

+ A climate and pollinator study that runs entirely in your browser. Pick the + settings, press run, and the same Python package used on the command line + analyses the observations and trains the forecasting models here. +

+

+ Simulated data. The observations below are generated from + assumed warming and decline rates to demonstrate the method. They are not + field measurements and support no scientific conclusion. +

+
+ +
+
+
+ Study + + + + + +
+
+ View + + +
+
+ +

+ 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, + }); +});