|
| 1 | +--- |
| 2 | +name: annual-cloud-data-update |
| 3 | +description: >- |
| 4 | + Update the real-time-cloud Cloud_Region_Metadata.csv with a new year of cloud-provider |
| 5 | + sustainability data (PUE, WUE, carbon-free / renewable energy %, grid carbon intensity) |
| 6 | + when AWS, Google, Microsoft Azure or Oracle publish their annual reports. Use this whenever |
| 7 | + the user wants to add or refresh a year of cloud region metadata, extract PUE/WUE/CFE from a |
| 8 | + provider sustainability report, reconcile a provider's disclosures into this dataset, or open |
| 9 | + the annual update PRs. Providers publish on a rolling schedule (Azure ~May, Google ~June, |
| 10 | + Amazon ~July, Oracle ~Dec), so this is typically run several times a year as each vendor lands. |
| 11 | +--- |
| 12 | + |
| 13 | +# Annual cloud region metadata update |
| 14 | + |
| 15 | +This repo (`Green-Software-Foundation/real-time-cloud`) normalises annual sustainability metadata |
| 16 | +from the major cloud providers into one CSV, `Cloud_Region_Metadata.csv`, on the schema described |
| 17 | +in `Cloud_Region_Metadata_specification.md`. Each year the providers publish a new year of data and |
| 18 | +this file gets extended. This skill captures how to do that cleanly, because the work is fiddly and |
| 19 | +several mistakes are easy to make and hard to spot. |
| 20 | + |
| 21 | +Read this file, then read the per-provider reference for whichever vendor(s) you're updating: |
| 22 | +`references/aws.md`, `references/google.md`, `references/azure.md`, `references/oracle.md`. The two |
| 23 | +output docs have templates: `references/metrics-doc-template.md` and `references/gaps-report-template.md`. |
| 24 | + |
| 25 | +## The two rules that matter most |
| 26 | + |
| 27 | +**1. Additive only.** Never modify or delete a row for a year that's already published. Providers |
| 28 | +sometimes *restate* older numbers or *blank* a value in a later snapshot — do not chase those into |
| 29 | +the historical rows. A correct update has `removed: 0` in the diff check below. If the user asks to |
| 30 | +fix an older year, do it as an explicit, separate decision, not as a side effect. |
| 31 | + |
| 32 | +**2. Per-region physical data only — not the global market claim.** Most providers claim "100% |
| 33 | +renewable/carbon-free" on a company-wide, market-based basis that nets purchases across grids that |
| 34 | +aren't electrically connected. That claim is **not useful per-region** and must not populate the |
| 35 | +carbon-free-energy columns. Specifically: |
| 36 | +- `provider-cfe-hourly` / `provider-cfe-annual` are for *location-based, per-region* carbon-free |
| 37 | + energy. Only fill them with a genuine per-region figure (e.g. Google's hourly CFE, Oracle's |
| 38 | + per-region renewable %). Do **not** set a uniform `1.0` because the provider claims global 100%. |
| 39 | +- `provider-carbon-intensity-market-annual` is the dedicated market column. It's acceptable to set |
| 40 | + it to `0` **only** where the provider publishes a genuine *per-region* 100% list (AWS matches |
| 41 | + energy within specific grids and names those regions). Carry that per-region, never as a uniform |
| 42 | + value. |
| 43 | + |
| 44 | +When in doubt about whether a figure is per-region-physical or a global-market claim, leave it blank |
| 45 | +and flag it for the user — a blank is honest, an invented number is not. |
| 46 | + |
| 47 | +## Workflow: three phases |
| 48 | + |
| 49 | +The work runs in three phases — **extract → merge → gaps** — because the value of this project isn't |
| 50 | +just the merged table, it's making the providers' metrics *comparable*. If you jump straight to the |
| 51 | +CSV you lose the definitions, and the definitions are where the incomparability hides (one vendor's |
| 52 | +"PUE" is a 12-month rolling average across 88% of sites; another's is Jan–Dec for operated sites; a |
| 53 | +"100% renewable" claim can be global-market or per-grid). So capture the definitions first, merge only |
| 54 | +what genuinely fits the schema, and write up the gaps. |
| 55 | + |
| 56 | +Outputs land in `metrics/<year>/`: one `<vendor>-<year>-metrics.md` per provider, and one shared |
| 57 | +`gaps-<year>.md`. |
| 58 | + |
| 59 | +### Phase A — Extract data *and definitions* into `<vendor>-<year>-metrics.md` |
| 60 | +For each vendor, read the source report(s) (see the per-provider reference for exact URLs) and produce |
| 61 | +a standalone Markdown doc that a reader could use *without* the original PDF. For every metric capture: |
| 62 | +- **Metric** and the **value(s)** per region/scope, with units. |
| 63 | +- **The vendor's own definition, quoted** (e.g. AWS: PUE/WUE "for data centers operated by AWS between |
| 64 | + January 1 and December 31 of each year"). Quote it verbatim — the definition is the point. |
| 65 | +- **Reporting period — exact dates, not just a year label**: AWS and Google report calendar year |
| 66 | + (Jan–Dec); **Azure (FY Jul–Jun) and Oracle (FY Jun–May) report fiscal years that end mid-calendar-year**, |
| 67 | + so their "2025" ends ~mid-2025 and is a different window than a calendar-2025 figure. Also the |
| 68 | + boundary (operated vs owned+leased, % of sites covered, rolling-12-month vs annual). Record the exact |
| 69 | + period per metric — sometimes different tables in one report use different bases. |
| 70 | +- **Method**: location-based vs market-based; hourly vs annual; per-region vs macro/fleet-wide. |
| 71 | +- Anything that has **no home in the RTC schema** (e.g. Oracle's tCO₂e/$USD, AWS embodied carbon) — |
| 72 | + record it so the gaps report can note it. |
| 73 | + |
| 74 | +Follow the standard `metrics-doc` template in `references/metrics-doc-template.md`. This doc is the |
| 75 | +input to both later phases; get it right and the merge is mechanical. |
| 76 | + |
| 77 | +### Phase B — Merge conforming metrics into the metadata table |
| 78 | +Only metrics whose definition genuinely matches the schema column's intended meaning get merged (see |
| 79 | +`Cloud_Region_Metadata_specification.md` for what each column means, and rule 2 above for the |
| 80 | +CFE/market distinction). If a vendor's metric almost-but-not-quite fits, prefer leaving the cell blank |
| 81 | +and recording the mismatch in the gaps report over forcing a number in. The mechanics (build script, |
| 82 | +carry-forward, verify) are steps 1–5 below. |
| 83 | + |
| 84 | +### Phase C — Write `gaps-<year>.md` |
| 85 | +A cross-vendor alignment report: for each metric in the schema, tabulate how each vendor defines and |
| 86 | +reports it, then call out what each vendor should change to make it comparable. This directly serves |
| 87 | +the project's mission of lobbying providers toward a common definition. See |
| 88 | +`references/gaps-report-template.md` for the structure and the recurring gaps to check every year |
| 89 | +(fiscal-vs-calendar year, PUE boundary/coverage, WUE availability, location-vs-market CFE, per-region |
| 90 | +vs macro/fleet reporting, missing per-region grid data). |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Merge mechanics (Phase B, step by step) |
| 95 | + |
| 96 | +### 1. Set up a clean working base |
| 97 | +The authoritative state is `upstream/main` (GSF). Local `main` and the fork's `Dev` drift behind it. |
| 98 | +``` |
| 99 | +git fetch upstream && git fetch origin |
| 100 | +git checkout main && git merge --ff-only upstream/main # if local main is behind and not ahead |
| 101 | +git checkout -b <year>-<provider>-update upstream/Dev # PRs target Dev; Dev == main data-wise |
| 102 | +``` |
| 103 | +Confirm `Dev` and `main` hold identical data files before branching off `Dev` (they usually do; the |
| 104 | +few `main`-only commits are release merges / issue templates). One provider per branch/PR keeps |
| 105 | +review clean, unless the user wants them combined. |
| 106 | + |
| 107 | +### 2. Find the source data |
| 108 | +Each provider has a tracking issue where the report link (and sometimes an extracted table) is |
| 109 | +posted — check open issues labelled `action item` (e.g. AWS #158, Google #159, Azure #161, Oracle |
| 110 | +#86). The report itself is the source of truth; a pasted table in an issue is a convenience. See the |
| 111 | +per-provider reference for exact URLs, which page/table to read, and the region-name→region-code |
| 112 | +mapping. |
| 113 | + |
| 114 | +### 3. Build the new rows |
| 115 | +Prefer a small, explicit, reviewable Python script that encodes the source table and writes rows, |
| 116 | +over the older scraper scripts in `code/` (`aws-data-update.py` scrapes a popup and has historically |
| 117 | +mislabelled columns; trust the report/issue table instead). `gcp-data-update.py` pulling from the |
| 118 | +`region-carbon-info` repo is reliable for Google's CFE/grid data. |
| 119 | + |
| 120 | +Carry forward slowly-changing metadata (`cfe-region`, `em-zone-id`, `wt-region-id`, `location`, |
| 121 | +`geolocation`, and grid carbon intensity) from the region's most recent prior year — no new |
| 122 | +Electricity Maps / WattTime lookup happens here. Only populate PUE/WUE/CFE for the new year where |
| 123 | +the provider actually disclosed it. |
| 124 | + |
| 125 | +**New regions appear every year** (each provider launches several). A region with disclosed data but |
| 126 | +no prior row has nothing to carry forward — add it fresh rather than skipping it: **geocode a |
| 127 | +city-level geolocation** (`geopy` Nominatim on the city name, rounded to 4 dp — the dataset uses |
| 128 | +city-level, not exact datacenter coordinates), set `cfe-region`/`em-zone-id` from the country/grid |
| 129 | +(the Electricity Maps zone), fill the disclosed PUE/WUE/CFE, and leave `wt-region-id` + grid carbon |
| 130 | +intensity blank with a note that they need a WattTime / Electricity Maps lookup. Adding the region now |
| 131 | +with partial metadata is better than dropping it — the grid columns can be filled when the lookup runs. |
| 132 | + |
| 133 | +**Water (`total-water-input`).** Some providers report a per-region annual water **withdrawal** in |
| 134 | +litres (AWS does, on each fact sheet). That maps to `total-water-input`. Watch the reporting boundary: |
| 135 | +where a provider reports by **country** but has several regions there, allocate/interpolate — if the |
| 136 | +regions are reported separately (AWS India: Mumbai/Hyderabad) use each; if combined (AWS Japan = |
| 137 | +Tokyo+Osaka together) allocate to the primary region and leave the other blank (or split on a stated |
| 138 | +basis), and document the choice. Convert gallons to litres where needed. |
| 139 | + |
| 140 | +**Grid data (Electricity Maps / WattTime).** `em-zone-id`/`wt-region-id` are the lookup keys; the actual |
| 141 | +`grid-carbon-intensity-*` values come from those services. Electricity Maps publishes yearly zone data |
| 142 | +but via an **account-gated free portal** (`app.electricitymaps.com/datasets`, ~5 downloads) — not a |
| 143 | +fetchable URL; WattTime marginal data is **API-gated** (credentials). So for a brand-new region set the |
| 144 | +`em-zone-id` and leave grid carbon blank, and flag it for whoever has portal/API access to fill (the |
| 145 | +project has done this before — see issue #86). Don't block the row on it. |
| 146 | + |
| 147 | +**Critical file-format details** (getting these wrong makes the diff look enormous): |
| 148 | +- The file is **CRLF**. Always write with `df.to_csv(path, index=False, lineterminator="\r\n")`. |
| 149 | +- Read with `dtype=str` so existing numeric strings aren't reformatted. |
| 150 | +- Sort `year` descending, then provider, then region (matches the repo convention). |
| 151 | +- The `geolocation` field contains a comma inside quotes, so **never parse this CSV with `awk -F,`** |
| 152 | + — it mis-splits every subsequent column. Use Python's `csv` module or pandas. |
| 153 | + |
| 154 | +### 4. Verify before committing |
| 155 | +``` |
| 156 | +python3 -c "import csv;r=list(csv.reader(open('Cloud_Region_Metadata.csv')));n=len(r[0]);\ |
| 157 | +print('cols',n,'bad',[i for i,x in enumerate(r) if len(x)!=n][:5])" # expect 24 cols, no bad rows |
| 158 | +grep -c $'\r' Cloud_Region_Metadata.csv # CRLF preserved on every line |
| 159 | +diff <(git show upstream/Dev:Cloud_Region_Metadata.csv|sort) <(sort Cloud_Region_Metadata.csv)|grep -c '^<' # MUST be 0 (additive) |
| 160 | +``` |
| 161 | +Then spot-check a handful of rows against the source report (right PUE in the PUE column, right WUE |
| 162 | +in WUE, CFE where expected, market column per the rules above). |
| 163 | + |
| 164 | +### 5. Regenerate the estimate and update the README |
| 165 | +The published estimate table is the **complete best-guess** current-year table — every region gets a |
| 166 | +row and every tracked metric is filled (trended where the region has history, otherwise a regional |
| 167 | +best guess). Generate it with: |
| 168 | +``` |
| 169 | +python3 code/complete_estimate.py Cloud_Region_Metadata.csv <next-year> # e.g. 2026 |
| 170 | +``` |
| 171 | +Fill rules that matter: **grid carbon intensity** is a physical grid property, so fill it from the same |
| 172 | +`em-zone-id` / continent (across providers is fine — it's the same grid). **Provider-specific metrics** |
| 173 | +(PUE, WUE, CFE %, market carbon, water) fill **only from the same provider's** regional data — **never |
| 174 | +estimate one provider's metric from another's**; if a provider doesn't report a metric anywhere (AWS/Azure |
| 175 | +CFE %, Google/Azure water, Google WUE), leave it blank. The never-reported EU-EED fields |
| 176 | +(`total-ICT-energy-consumption-annual`, `renewable-energy-*`) also stay blank. **Keep the reported table (`Cloud_Region_Metadata.csv`) clean — never fill best-guesses |
| 177 | +into it; the estimate table is the only place guesses belong.** (`estimate_current_region_metadata.py` |
| 178 | +is the older trend-only estimator, kept for reference/testing; it leaves gaps blank.) Update the |
| 179 | +"Cloud Providers" paragraph in `README.md` to state the latest real year and the projected year. |
| 180 | + |
| 181 | +### 6. Open the PR and update the tracking issue |
| 182 | +Base the PR on `Dev`. Include the CSV/estimate/README changes **and** the phase-A `metrics/<year>/` |
| 183 | +docs and the phase-C `gaps-<year>.md` — they're deliverables, and they're the evidence for every |
| 184 | +merge decision and caveat. In the body, state the sources (with links), the per-provider column |
| 185 | +mapping, and every caveat/approximation. Comment on the provider's tracking issue with what landed and |
| 186 | +what's still missing. Commit trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`; |
| 187 | +PR-body trailer: `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. |
| 188 | + |
| 189 | +### 7. Archive the source documents |
| 190 | +Providers periodically move or delete these files (the AWS popup, gstatic-hosted Google reports and |
| 191 | +Oracle's data sheets have all changed URLs). Save the PDFs you used to `sources/<data-year>/<provider>/` |
| 192 | +with a `sources/README.md` manifest (document each file's source URL, retrieval date, and a **gaps table** |
| 193 | +for documents still needed — e.g. Oracle's CY report, Electricity Maps/WattTime data, prior-year fact |
| 194 | +sheets). Download prior years too where the URLs are still live. This is bulky (~180 MB/year of PDFs); |
| 195 | +keep it on its own branch/PR and flag **Git LFS or an external release-asset store** vs a plain commit so |
| 196 | +the WG decides before merging binaries into history. |
| 197 | + |
| 198 | +## Tooling notes |
| 199 | +- PDFs: `pdfplumber` (reports) and `openpyxl` (Google-Sheet exports of working data). A working |
| 200 | + `venv` may have a Python-version/site-packages mismatch — if `import` fails, run the system |
| 201 | + `python3` with `PYTHONPATH=code/venv/lib/python3.XX/site-packages` pointing at wherever the |
| 202 | + packages actually installed. |
| 203 | +- Google Sheets shared by the user export as CSV/XLSX via |
| 204 | + `https://docs.google.com/spreadsheets/d/<ID>/export?format=xlsx`. |
| 205 | +- The scratchpad dir (per session) is the place for these one-off builder scripts, not the repo. |
| 206 | + |
| 207 | +## Outputs of a full run |
| 208 | +A complete update produces, and the PR(s) should include: new **reported** rows in |
| 209 | +`Cloud_Region_Metadata.csv` (clean, real data only); the regenerated **complete estimate** |
| 210 | +`Cloud_Region_Metadata_estimate.csv` (best-guess, every cell filled); the phase-A |
| 211 | +`metrics/<year>/<vendor>-<year>-metrics.md` docs and phase-C `metrics/<year>/gaps-<year>.md`; a README |
| 212 | +update; and the `sources/<year>/` archive (own branch). Reported table clean, estimate table filled — |
| 213 | +never mix the two. |
| 214 | + |
| 215 | +## What each provider gives you (summary — details in the references) |
| 216 | +| Provider | Per-region disclosed | Notes | |
| 217 | +|----------|----------------------|-------| |
| 218 | +| AWS | PUE, WUE, water withdrawal (per-region); per-grid 100%-renewable list | `references/aws.md` — **~32 per-region PDF fact sheets** to download + process individually; friendly-name→code map; market=0 only for listed regions; regional-average PUE for newer regions | |
| 219 | +| Google | hourly CFE + grid carbon (region-carbon-info repo); per-datacenter PUE (report PDF) | `references/google.md` — annual CFE left blank; don't use the 100% claim | |
| 220 | +| Azure | only macro-regional (Americas/APAC/EMEA), fiscal-year PUE/WUE; detailed data fact sheet PDF for definitions | `references/azure.md` — map by continent; no per-region since 2022 | |
| 221 | +| Oracle | renewable % per region; PUE (CY report); grid via Electricity Maps | `references/oracle.md` — key→identifier map; gov regions inherit co-located grid | |
| 222 | +| Others | IBM/Alibaba/OVHcloud publish only fleet-wide PUE — not usable per-region | skip unless per-region data appears; still note them in the gaps report | |
0 commit comments