Paris Agreement NDC pledges vs actual energy trajectory gap analysis
Features • Architecture • Quick Start • Development • Deployment • Contributing
DeltaGrid is an interactive dashboard that calculates the gap between Paris Agreement NDC pledges and actual energy transition trajectories for 200+ countries. Users adjust slider weights for energy sources (solar, wind, hydro, nuclear, gas, coal) and see countries re-ranked in real time.
The problem: NDCs (Nationally Determined Contributions) set emission targets, but tracking progress country-by-country requires comparing apples-to-oranges: intensity targets vs absolute targets, different base years, different sector coverage. DeltaGrid normalizes these into a single 0–100 Green Score and computes the gap to each country's pledged trajectory.
- Green Score — weighted composite (0–100) from 6 energy shares, normalized to absolute scale
- Gap Analysis — actual score vs linear NDC trajectory, per country and year
- Real-time Re-ranking — move a slider, the map and tables update immediately
- World Map — Plotly choropleth maps for green score and gap
- Custom Data Upload — CSV/XLSX with auto-preprocessing: encoding detection, column normalization, ISO code mapping
- Classification — countries bucketed into hidden champions, on track, slightly behind, laggards, or no data
┌─────────────────────────────────────────────────┐
│ app/ │
│ main.py · 1_gap_analysis.py · 2_rankings.py │
│ 3_methodology.py · sidebar.py · choropleth.py │
│ tables.py · ui.py │
│ ── Streamlit pages + components │
├─────────────────────────────────────────────────┤
│ src/ │
│ scoring.py · gap.py · ranking.py · pipeline.py │
│ ── Computation, scoring, gap, classification │
├─────────────────────────────────────────────────┤
│ src/data/ │
│ owid.py · climate_watch.py · validators.py │
│ cache.py · country_codes.py │
│ upload_preprocessor.py │
│ ── Ingestion, API, caching, preprocessing │
└─────────────────────────────────────────────────┘
Sidebar (weights, year)
│
├─→ compute_green_score() ──→ choropleth (main page)
│
├─→ fetch_all_ndcs()
│ │
│ └─→ compute_gap() ──→ choropleth + stats (gap analysis page)
│ │
│ └─→ classify_countries() ──→ ranking tables (rankings page)
| Decision | Rationale |
|---|---|
| 5 dependencies only | streamlit, plotly, pandas, requests, numpy. No geopandas, no paid APIs, no heavy frameworks |
| Plotly px.choropleth, not geopandas | GeoJSON bundling is fragile; Plotly's built-in country outlines cover 200+ countries with zero setup |
| Green score normalization | score / max(all_weights) instead of score / score.max() * 100. Dividing by max weight means 100% from the top source = 100, and slider changes rescale the entire map visibly |
| No ORM, no database | The dataset is small enough (4,500 rows) that pandas in memory is faster and simpler than any persistence layer |
@st.cache_data for Streamlit |
Memoizes scoring, analysis, and choropleth figures across reruns. OWID CSV cached via @st.cache_data(ttl=3600), NDC API cached to disk (24h TTL) |
- Python 3.10+ (tested on 3.12)
- pip
git clone https://github.com/AshayK003/DeltaGrid.git
cd DeltaGrid
# Create virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
# Install
pip install -r requirements.txt
pip install -e ".[dev]" # dev tools (ruff, mypy, pytest)streamlit run app/main.pyOpen http://localhost:8501. The first load reads the filtered OWID energy CSV from data/raw/ (tracked in repo, ~2.4 MB).
make install # pip install -r requirements.txt + dev extras
make test # pytest tests/ -v (138 tests)
make lint # ruff check src/ app/
make typecheck # mypy src/ app/ (strict mode)
make serve # streamlit run app/main.py
make clean # remove __pycache__, .pytest_cache, .mypy_cache, .ruff_cacheDeltaGrid/
├── app/ # Streamlit application
│ ├── main.py # Entry point, cached scoring, CSS, metrics
│ ├── components/
│ │ ├── sidebar.py # Weight sliders, year selector, file upload
│ │ ├── choropleth.py # Plotly world map component
│ │ ├── tables.py # Ranking tables with conditional formatting
│ │ └── ui.py # Shared UI: headers, errors, badges, footer
│ └── pages/
│ ├── _shared.py # Cached analysis wrapper + load_energy_data()
│ ├── 1_gap_analysis.py
│ ├── 2_rankings.py
│ └── 3_methodology.py
├── src/
│ ├── config.py # Constants, column names, default weights, thresholds
│ ├── pipeline.py # AnalysisResult dataclass, run_analysis() orchestrator
│ ├── data/
│ │ ├── owid.py # OWID CSV download and filtering
│ │ ├── climate_watch.py# NDC bulk fetch + _parse_ghg_percentage()
│ │ ├── cache.py # TTL disk cache (JSON files)
│ │ ├── country_codes.py# ISO normalization, aggregate detection
│ │ ├── validators.py # Schema validation, energy/NDC merge
│ │ └── upload_preprocessor.py
│ └── models/
│ ├── scoring.py # compute_green_score(weights required)
│ ├── gap.py # compute_gap() with vectorized interpolation
│ └── ranking.py # classify_gap(), classify_countries()
├── tests/ # 138 tests across 10 modules
├── data/
│ ├── raw/ # OWID CSV (gitignored — downloaded on first run)
│ └── cache/ # JSON cache files (gitignored)
├── .streamlit/config.toml # Dark theme, headless mode
└── Makefile # Dev workflow commands
| Source | What | Access |
|---|---|---|
| Our World in Data — Energy | Country-level energy mix (1900–2025) | CSV download, cached 7 days |
| Climate Watch — NDC API | GHG targets, base/target years, conditionality | REST API (bulk fetch), cached 24 hours |
green_score = Σ(share_i × weight_i) / max(all_weights)
- share_i: energy source as percentage of total (0–100)
- weight_i: user-adjustable slider (0.0–2.0, default per source)
- Output is absolute 0–100: 100 means 100% of energy comes from the highest-weighted source
| Source | Default | Why |
|---|---|---|
| Solar | 1.0 | Zero-emission, fastest-growing |
| Wind | 1.0 | Zero-emission, rapidly scaling |
| Hydro | 1.0 | Zero-emission, established baseload |
| Nuclear | 0.5 | Low-carbon but controversial |
| Gas | 0.2 | Fossil (bridge fuel) |
| Coal | 0.0 | Highest-emission fossil |
gap = actual_green_score - expected_trajectory
expected_trajectory = linear_interpolation(
base_value=0,
target_value=NDC_ghg_target_percent,
base_year=NDC_pledge_base_year,
target_year=NDC_pledge_target_year,
current_year=selected_year,
)
| Class | Gap | Meaning |
|---|---|---|
| Hidden Champion | > 5 | Significantly ahead of NDC trajectory |
| On Track | 0 to 5 | Meeting or slightly ahead |
| Slightly Behind | -5 to 0 | Within striking distance |
| Laggard | < -5 | Far behind trajectory |
| No Data | missing | No NDC data available |
# Run all tests
make test
# Run a specific module
pytest tests/test_scoring.py -v
# Run with coverage
pytest tests/ --cov=src --cov-report=term-missingTest coverage by module:
| Module | Tests | What it covers |
|---|---|---|
test_scoring.py |
9 | Empty/NaN/single-row DataFrames, zero weights, range bounds, mutation safety |
test_gap.py |
6 | Positive/negative gaps, missing NDCs, invalid years, NaN scores, empty input |
test_ranking.py |
17 | Boundary classifications (exactly 5, 0, -5), empty results, missing columns |
test_climate_watch.py |
21 | Percent parser (range/dash/float/keyword), network failures, cache behavior |
test_cache.py |
10 | TTL expiry, corrupted JSON, key sanitization, empty dir |
test_country_codes.py |
17 | ISO normalization, aggregates, whitespace, mixed case |
test_validators.py |
12 | Missing fields, partial overlap, empty dicts |
test_owid.py |
4 | Year range, CSV loading, aggregate filtering |
test_upload_preprocessor.py |
33 | Encoding, column normalization, ISO mapping, alternative columns, full pipeline |
test_integration.py |
8 | End-to-end pipeline, weight-specific rankings, NDC-less countries |
- Push repo to GitHub
- Go to share.streamlit.io
- Connect repo, set Main file path to
app/main.py - Deploy — no additional config needed
streamlit run app/main.py --server.port 80 --server.address 0.0.0.0No environment variables, secrets, or API keys required. All data sources are public. The app runs entirely on:
- OWID energy CSV (CC BY 4.0)
- Climate Watch NDC API (open access)
The sidebar accepts CSV or XLSX files. Required columns:
| Column | Description |
|---|---|
iso_code |
ISO-3166-1 alpha-3 (e.g., IND, USA) |
year |
Integer year |
Optional columns (for green score computation):
| Column | Alias alternatives |
|---|---|
solar_share_energy |
solar, solar_share, solar_pct |
wind_share_energy |
wind, wind_share, wind_pct |
hydro_share_energy |
hydro, hydro_share, hydro_pct |
nuclear_share_energy |
nuclear, nuclear_share |
gas_share_energy |
gas, gas_share, natural_gas |
coal_share_energy |
coal, coal_share |
Column names are normalized (lowercased, snake_cased). Missing ISO codes and aggregate regions (e.g., "World", "Africa") are removed automatically.
- Read AGENTS.md — contains the full agent context with all conventions, bug history, and design rationale
- Open an issue first for any non-trivial change
- Branch from
master:git checkout -b feat/your-feature - Write tests first for new functions (fixtures, edge cases, error paths)
- Run the full suite before opening a PR:
make lint && make typecheck && make test
- Keep dependencies lean — no new dependency without discussion. The 5-dependency constraint is deliberate
- Ruff linting (E, F, I, N, W, UP rulesets). Run
make lintbefore commit - mypy strict mode. Run
make typecheckbefore commit - No comments on obvious code — prefer readable names over explanatory comments
- Function composition over inheritance
- No unnecessary abstractions — "keep it boring"
| Problem | Cause | Fix |
|---|---|---|
ImportError: cannot import name 'X' |
Stale __pycache__ |
make clean |
| Map is all one color | Fixed color range (vmin/vmax) was too wide for data range | Removed in v0.1.3 — now auto-scales |
| Slider changes don't affect map | Old normalization (score / max * 100) compressed all scores to 0–100 regardless of weights |
Fixed v0.1.3: score / max(all_weights) — changing max weight rescales all scores visibly |
| OWID CSV fails to download | Network issue or GitHub rate limit | CSV is cached in data/raw/ after first successful download |
| NDC API returns empty | Network issue or API downtime | App continues with gap = green_score for all countries |
| Uploaded file columns not recognized | Column names don't match expected patterns | Check normalization: names are lowercased, underscores for spaces. Check _detect_alternative_columns() in upload_preprocessor.py |
MIT — see LICENSE (or the MIT template).
If DeltaGrid helps your climate research or policy work and you'd like to support the developer: