DEV Community

Cover image for How I built DeltaGrid: a Paris Agreement gap analysis dashboard with 5 dependencies and zero paid APIs
BrainWire
BrainWire

Posted on

How I built DeltaGrid: a Paris Agreement gap analysis dashboard with 5 dependencies and zero paid APIs

The Paris Agreement is full of pledges. What it lacks is a simple way to see whether anyone is actually keeping them.
I built DeltaGrid to answer that. It calculates the gap between each country's NDC pledge and their actual energy transition trajectory, lets you adjust how you weight different energy sources, and shows you the result on a world map in real time.
200+ countries. 138 tests. 5 dependencies.

The normalization problem

NDCs (Nationally Determined Contributions) cannot be compared directly. Some countries pledge intensity reductions (emissions per unit of GDP), others pledge absolute cuts. Base years differ. Some pledges cover electricity only, others the full economy.
To compare them, I normalize everything into a Green Score from 0 to 100 and compute a gap against each country's pledged trajectory.

The Green Score formula

pythongreen_score = sum(share_i * weight_i) / max(all_weights)
share_i is the percentage of a country's energy from source i. weight_i is a user-adjustable slider between 0.0 and 2.0.
Dividing by max(all_weights) instead of score.max() * 100 keeps the output on an absolute scale. This is important: if you lower the weight of coal, countries that rely on coal see their score drop visibly. The map responds to your choices in a way that actually means something.
I had this wrong in the first version. The old normalization (score / score.max() * 100) compressed all scores into 0 to 100 regardless of weight changes. Sliders felt broken because they barely moved the map. Switching to max-weight normalization fixed it immediately.
Default weights:
SourceWeightWhySolar1.0Zero emission, fastest growingWind1.0Zero emission, rapidly scalingHydro1.0Zero emission, established baseloadNuclear0.5Low carbon but controversialGas0.2Fossil fuel, bridge roleCoal0.0Highest emission fossil

The gap formula

pythongap = 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
)
NDC data comes from the Climate Watch API. The bulk fetch is cached to disk for 24 hours. Parsing GHG targets from the raw API response is messy: values come in as ranges ("30-40%"), dashes, floats, or keywords. The _parse_ghg_percentage() function handles all of these cases and has 21 dedicated tests in test_climate_watch.py.
Classification thresholds after gap is computed:
ClassGapHidden Champion> 5On Track0 to 5Slightly Behind-5 to 0Laggard< -5No Datamissing

The 5-dependency constraint

The entire app runs on: streamlit, plotly, pandas, requests, numpy.
Every dependency I considered adding had a cost: geopandas adds system-level binaries and makes cloud deployment fragile. A database adds a persistence layer the dataset does not need. An embeddings library adds an API call for something that does not require semantic search.
The dataset is 4,500 rows. Pandas in memory is the right tool.
For the world map, Plotly's px.choropleth has built-in country outlines covering 200+ countries. No GeoJSON bundling, no shapefile management, no projection configuration. It just works with an ISO-3166 alpha-3 column.
Both data sources (Our World in Data energy CSV and Climate Watch NDC API) are free and open access. No API keys required anywhere in the app.

Architecture: three layers

app/ # Streamlit pages and components
src/ # Computation: scoring, gap, ranking, pipeline
src/data/ # Ingestion, caching, validation, preprocessing
The data flow is linear:
sidebar weights + year
-> compute_green_score()
-> fetch_all_ndcs()
-> compute_gap()
-> classify_countries()
-> choropleth + tables
@st.cache_data memoizes scoring, gap analysis, and choropleth figures across reruns. The OWID CSV is cached with a 1-hour TTL. NDC API responses are cached to disk for 24 hours using a simple JSON-based TTL cache in src/data/cache.py.

Custom data upload

The sidebar accepts CSV or XLSX uploads. Column detection is fuzzy: a column called "solar_pct", "solar_share", or just "solar" all resolve to solar_share_energy. Encoding is auto-detected. ISO codes are normalized and aggregates (World, Africa, etc.) are filtered out automatically.
The upload preprocessor has 33 tests covering encoding edge cases, column normalization, ISO mapping, alternative column names, and the full preprocessing pipeline end to end.

Testing: 138 tests across 10 modules

ModuleTeststest_upload_preprocessor.py33test_climate_watch.py21test_ranking.py17test_country_codes.py17test_validators.py12test_cache.py10test_scoring.py9test_gap.py6test_owid.py4test_integration.py8
Integration tests cover end-to-end pipeline runs, weight-specific ranking behavior, and countries with no NDC data.

What I would do differently

The NDC parsing is the messiest part of the codebase. Climate Watch API responses are inconsistent enough that the parser handles 6 different value formats. A preprocessing step that normalizes raw API responses before they enter the scoring pipeline would have made this cleaner.
I would also add confidence intervals to the gap score earlier. A country that barely has NDC data should show more uncertainty than one with a full pledge and strong historical energy data. Right now they get the same classification treatment.

Deployment

Push to GitHub, connect to Streamlit Community Cloud, set main file to app/main.py. No secrets, no environment variables, no paid services.
bash# Local
streamlit run app/main.py

Dev workflow

make lint && make typecheck && make test

Contributing

Read AGENTS.md first. It has the full agent context including bug history, design decisions, and conventions.
The 5-dependency constraint is hard. Any PR adding a new dependency needs a strong argument. Everything else is open: new classification schemes, new data sources, better NDC parsing, UI improvements.
Repo: github.com/AshayK003/DeltaGrid
Try the app here: https://deltagrid.streamlit.app/

Top comments (0)