Skip to main content
Glama
README.md
# AMR-Climate Atlas — Causal Attribution of Climate to Antimicrobial Resistance

Project 2 of a three-project climate-health portfolio (see
[`../harvard-dataverse-climate-health-projects.md`](../harvard-dataverse-climate-health-projects.md)).
Scoping: [`../amr-climate-causal-attribution-scoping.md`](../amr-climate-causal-attribution-scoping.md) ·
Build plan: [`../amr-climate-causal-attribution-phases.md`](../amr-climate-causal-attribution-phases.md) ·
Full write-up: [`docs/paper.md`](docs/paper.md).

**Status: all four phases complete.** EU/EEA panel (29 countries, 2000–2023, 4 pathogen-antibiotic
combinations), a properly-tested causal model, a genomic cross-check, a live Streamlit app, an MCP
server exposing the live data sources, and an automated eval-gated validation pipeline.

![App screenshot: K. pneumoniae/Carbapenems combo, Greece 2023 highlighted at the top of the resistance distribution, with the causal-test caveat surfaced above the chart](docs/screenshots/app_screenshot.jpg)

## The finding

A two-way (country + year) fixed-effects panel regression, with a **mandatory 200+ permutation
placebo test** on every result before it's allowed to be called significant:

| Combo | n | Coef | Parametric p | Permutation p |
|---|---|---|---|---|
| E. coli / Fluoroquinolones | 642 | +0.11 | 0.77 | 0.62 |
| E. coli / 3rd-gen cephalosporins | 642 | +0.21 | 0.54 | 0.31 |
| **K. pneumoniae / Carbapenems (2010-2022, consumption-adjusted)** | 355 | **−1.73** | **0.008** | **0.000** |
| S. aureus / MRSA | 667 | −0.13 | 0.89 | 0.69 |

**3 of 4 combinations show no weather effect anywhere.** The one exception — K. pneumoniae
carbapenem resistance, in the ESAC-Net-consumption-adjusted 2010–2022 subsample — is real
(leave-one-country-out stable, not COVID-driven) and **directionally confirmed by independent
genomic sequencing data** (NCBI Pathogen Detection/MicroBIGG-E, same negative sign, r=+0.38
correlation with the phenotypic rate), but does **not** appear in the full 2000–2023 panel, and
antibiotic consumption itself isn't significant in the model that's supposed to control for it.
Read as: a real, narrow, sample-window-specific pattern, not a confirmed causal climate→AMR link.

This result survived a real QA process — an independent testing pass caught a missing permutation
test on the consumption-adjusted model, and a later robustness check caught a confounder
column-selection bug that meant antibiotic consumption was never actually in the model as first
reported. Both are disclosed plainly in `docs/paper.md` rather than smoothed over.

## What's in this repo

```
app.py                         Streamlit atlas — combo explorer, causal-test caveat, rankings
mcp_server/                    live-data MCP server (NASA POWER + BigQuery, not the frozen CSV)
data/
  eu_eea_pilot_panel.csv       joined country-year-combo panel (join_panel.py's output)
  validation_baseline.json     accepted "known good" snapshot for the eval-gate pipeline
  raw/                         gitignored raw pulls (ECDC, NASA POWER, ESAC-Net, EPI)
validation_runs/               timestamped JSON logs from validate_pipeline.py runs
docs/
  phase1_access_check.md       access-verification memo
  phase3_scoping.md            causal-design scoping, grounded in the real pilot data
  phase4_scoping.md            scale-up fork (WHO GLASS vs. BigQuery vs. deepen EU/EEA) + genomic cross-check
  paper.md                     full write-up: methods, bugs found and fixed, results
src/amr_climate_atlas/
  config.py                    country lists, centroids, DOIs, ECDC/NASA POWER/BigQuery constants
  fetch_ears_net.py            EARS-Net resistance rates via ECDC Atlas (fully scriptable, see below)
  fetch_esac_net.py            antibiotic-consumption confounder (static XLSX annexes, 2010-2022)
  fetch_weather_anomaly.py     daily weather per EU/EEA country centroid (NASA POWER)
  fetch_confounders.py         EPI 2024 tabular confounder from Harvard Dataverse
  join_panel.py                joins resistance + weather + EPI into the country-year panel
  stage3_identification.py     two-way FE model + mandatory permutation test + lag check, per combo
  compare_genomic_phenotypic.py genomic (BigQuery) vs. phenotypic (EARS-Net) cross-check
  validate_pipeline.py         automated eval-gate: rerun + placebo-sanity-check + baseline drift check
.github/workflows/validate.yml ready-to-run CI (fires once this repo is pushed to GitHub)
```

## Two findings worth flagging up front

1. **ECDC's Surveillance Atlas export is fully scriptable, not manual-UI-only.** The export fires a
   plain parameterized GET, and its `downloadTokenId` param is **not actually validated
   server-side** — any value works once a session cookie is bootstrapped. `fetch_ears_net.py` pulls
   the full country×year grid for a given pathogen-antibiotic combo in two HTTP requests.
2. **Dataverse's API 403s the default `python-requests` User-Agent** — undocumented anywhere, a
   browser-like UA is required (see `config.DATAVERSE_REQUEST_HEADERS`).

## Quickstart

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

PYTHONPATH=src python -m amr_climate_atlas.fetch_ears_net        # ECDC EARS-Net resistance rates
PYTHONPATH=src python -m amr_climate_atlas.fetch_esac_net        # antibiotic consumption confounder
PYTHONPATH=src python -m amr_climate_atlas.fetch_weather_anomaly # NASA POWER, EU/EEA centroids
PYTHONPATH=src python -m amr_climate_atlas.fetch_confounders     # EPI 2024 from Dataverse
PYTHONPATH=src python -m amr_climate_atlas.join_panel            # join panel
PYTHONPATH=src python -m amr_climate_atlas.stage3_identification # causal model + permutation test

streamlit run app.py                                            # the live atlas
```

## MCP server — live data, not just the frozen CSV

`mcp_server/` exposes two of this project's data sources over the Model Context Protocol, so any
MCP-compatible client (Claude Desktop, a custom agent) can query live weather or live genomic AMR
data on demand, rather than only reading `eu_eea_pilot_panel.csv`. Built on Anthropic's official
`mcp` Python SDK (not the fabricated "MCP 2.0"/"Agent Plugins" terminology an earlier draft of this
scope used — see `mcp_server/README.md` for the full terminology correction and a real API-drift
finding: the `FastMCP` class most tutorials use has been removed from the current SDK). Tested with
a genuine external-process client (`mcp_server/test_external_client.py`), not just an in-process
call — real weather data for Paris, real carbapenem-gene prevalence (16.83%) for French E. coli
isolates. See `mcp_server/README.md` for setup and a documented naming quirk (NCBI groups E. coli
under `"E.coli and Shigella"` in its own schema, not `"Escherichia coli"`).

## Automated eval-gated validation

`validate_pipeline.py` automates `stage3_identification.py`'s fit + permutation test into a
rerunnable gate: it fails if the permutation test's null distribution looks degenerate (a real
check for the shuffle logic silently breaking) or if results drift meaningfully from a stored
baseline. This is designed to have caught the confounder bug above automatically, not just when a
human happened to look closely. A GitHub Actions workflow (`.github/workflows/validate.yml`) is
ready to run this on every push, once the repo is on GitHub.

## Deferred / not fetched by default

GRDI v1.10 (raster GeoTIFFs, 932MB) and EnvClim (2.6GB geopackage) — gridded/sub-national products
disproportionate to a country-year panel. `fetch_confounders.py` prints their file listings so a
future subnational build can decide if the GIS-tooling investment is worth it.