Skip to content

Commit d339e88

Browse files
HopelynconsultGoldokpaobielinclaudefranchaise
authored
feat(governance): add Gebru-style datasheets for training datasets (#52)
* feat(governance): add SHAP explainability for segmentation predictions (#29) - Add governance module with SHAPExplainer class - Implement band-level and spatial attribution using DeepExplainer - Add /api/explain endpoint for SHAP-based explanations - Create 06_explainability.ipynb with visualization examples - Add shap>=0.42.0 to requirements.txt Closes #22 Co-authored-by: Linda Oraegbunam <obielinda@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * chore: bring repo up to open-source community standards (#16) * Add SECURITY.md for security policy and reporting Added a security policy document outlining supported versions, vulnerability reporting, scope, and disclosure policy. * chore: add PR template for contributor guidance Add a pull request template to guide contributors. * chore: add CODEOWNERS assigning @Goldokpa as default reviewer Added CODEOWNERS file to define code ownership and review assignments. * chore: add Dependabot config for pip, npm, and GitHub Actions Configured Dependabot for Python, GitHub Actions, and npm dependencies with specified schedules and reviewers. * chore: add CHANGELOG.md following Keep a Changelog format Document notable changes, additions, modifications, and removals for ClimateVision. * chore: add CITATION.cff to enable GitHub Cite this repository button Added citation file for ClimateVision software. * fix: replace #email placeholder in CODE_OF_CONDUCT with Security Advisory link This change updates the Code of Conduct document by removing the original content and replacing it with a new version that includes various sections on community standards, enforcement responsibilities, and guidelines. * chore: remove SETUP_COMPLETE.md (internal artifact not suited for public repo) * chore: remove internal team_docs (Francis_Umo_Role.pdf) from public repo * chore: remove internal team_docs (Olufemi_Taiwo_Role.pdf) from public repo * feat(models): add biomass and carbon-stock regression module (#46) Adds BiomassRegressor — a wrapper around sklearn RandomForest and xgboost.XGBRegressor that exposes a stable fit/predict/evaluate/save API for ClimateVision pipelines. Default feature ordering matches the spectral indices produced by the data preprocessor (NDVI, EVI, SAVI, NDMI, NBR, R, G, B, NIR, SWIR1). Also adds: - biomass_to_carbon / biomass_to_co2e helpers using IPCC defaults (carbon fraction 0.47, 44/12 ratio for CO2e). - evaluate_regression for RMSE, MAE, R^2, and MAPE. - estimate_biomass_from_indices for inference over a dict of per-pixel index arrays. - save() / load() round-trip via pickle. Co-authored-by: Francis Umo <franchaise@users.noreply.github.com> * feat(notebooks): add 03_carbon_analysis carbon stock estimation notebook (#47) End-to-end notebook that loads (or simulates) a biomass-labelled spectral dataset, trains a Random Forest BiomassRegressor, evaluates RMSE/MAE/R^2/MAPE, converts biomass predictions to carbon and CO2e using IPCC defaults, plots feature importances, and persists the model + metrics for the analytics API and the model-card pipeline. Falls back to a synthetic dataset when the labelled parquet file is not present, so the notebook is runnable in CI. Co-authored-by: Francis Umo <franchaise@users.noreply.github.com> * feat(notebooks): add 04_model_validation benchmarking notebook (#48) Validates segmentation predictions against reference masks and the biomass regressor against held-out labels across Amazon, Congo, and Southeast Asia. Computes IoU, F1, precision, recall, accuracy, and the regression metrics RMSE/MAE/R^2/MAPE. Aggregates per-region and mean values into a single benchmark_report.json that the governance CI gate and the model-card generator consume directly. Co-authored-by: Francis Umo <franchaise@users.noreply.github.com> * feat(notebooks): add 05_impact_reporting stakeholder template (#49) End-to-end impact reporting workflow: loads a deforestation mask, runs estimate_carbon for tonnes-of-CO2e and confidence intervals, computes a trend over the trailing 12 months, attaches validation metrics from 04_model_validation.ipynb when available, calls analytics.reporting.generate_report, and renders a stakeholder-ready Markdown narrative with headline numbers, trend, and validation section. Region/period/bbox are top-level constants so the same notebook can be re-run for Amazon, Congo, or Southeast Asia. Co-authored-by: Francis Umo <franchaise@users.noreply.github.com> * feat(inference): add parallel batch processor with per-job tracking (#40) Adds BatchProcessor for running inference over a list of sources in parallel. Tracks per-job state (queued -> running -> succeeded/failed), records timing and attempt counts, and appends each terminal job to a JSONL manifest as it finishes so long-running batches can be resumed or audited without waiting on the whole queue. Supports configurable retry on transient failures and an injectable inference_fn for testing and for swapping in batch_predict implementations later. * feat(inference): add subscription-driven deforestation alert generator (#41) AlertGenerator matches inference results against per-organisation subscriptions (region bbox + analysis_type + threshold) and fires alerts when the measured value crosses the threshold. Per-subscription cooldown windows deduplicate flapping signals. Severity is classified medium/high/critical based on how far the value is past the threshold. Channels are pluggable: register a delivery callable per channel name ('log', 'email', 'webhook', ...) and the dispatcher will route to all the channels each subscription opted into. Persists every fired alert to a JSONL log for replay and audit. * feat(api): add /api/reports and /api/anomalies admin endpoints (#42) Adds api/admin.py — a self-contained APIRouter exposing two read-only operational endpoints: - GET /api/reports — data-quality KPIs (run count, error rate, mean confidence, positive-fraction mean, alert count) over a configurable window. - GET /api/anomalies — list flagged anomaly/alert records, optionally filtered by severity and time window. Both read from the JSONL logs written by the audit logger and the alert generator. They never expose raw input payloads. Wired into the FastAPI app via include_router() in api/main.py. * feat(governance): add anomaly detection for inference outputs (#35) Hybrid detector that combines an Isolation Forest fitted on rolling prediction history with a statistical fallback (z-score + IQR fences) for the cold-start case. Persists feature history to JSONL and emits anomaly reports for human review. * feat(governance): add hash-chained audit logger for predictions (#36) Append-only JSONL audit trail. Each entry records the model version, SHA-256 hash of the input payload, summary statistics for the output, and a prev_hash linking back through the chain. verify_chain() walks the file and detects any tampered entry by recomputing the hash and checking the link to its predecessor. * feat(governance): add automated model card generator (#37) Builds Mitchell-style model cards from training config + evaluation metrics, with optional fairness report attached. Renders to both Markdown (for release notes / model registry) and JSON (for downstream tooling). Ships a CLI entrypoint at scripts/generate_model_card.py for the release CI pipeline. * feat(reports): add LLM-backed impact report generator (#38) Composes natural-language stakeholder reports from carbon analytics, SHAP attributions, validation metrics, and fairness flags. A deterministic template renderer is always available so the pipeline never blocks on a missing LLM provider; when an LLM callable is supplied (or CLIMATEVISION_LLM_PROVIDER is configured) it smooths the template into prose using the structured data block as ground truth. Includes a JSON sidecar so downstream tooling can ingest the report without re-parsing Markdown. * feat(governance): add release CI gate for metrics, fairness, and security (#39) scripts/governance_ci_gate.py reads evaluation metrics, an optional fairness report, and an optional security scan, and decides whether a release passes its governance thresholds. Exit code 1 fails the build. Thresholds default to a sensible baseline (IoU>=0.70, F1>=0.75, fairness score>=0.80, zero high/critical security findings) and can be overridden via --thresholds JSON. Renders a Markdown summary suitable for posting back to the PR. * feat(security): API security middleware, pipeline guard, and OWASP scanner (#34) * feat(security): add API security middleware OWASP-aligned controls layered onto FastAPI: - Per-API-key rate limiter (sliding window, configurable) - Payload size and Content-Length checks - bbox sanity validation (range, ordering, max area to block DoS-via-huge-GEE-queries) - File upload validation by magic bytes + extension whitelist - String input sanitisation against XSS, SQLi, template injection patterns - Security response headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection) * feat(security): add inference pipeline guard for adversarial inputs InputAnomalyDetector flags suspicious tiles before model forward pass: - Out-of-range pixel values, NaN/Inf, suspicious uniformity - Gradient analysis to catch noise injection or constant-image attacks PipelineGuard wraps inference with input + output checks: - Rejects predictions where one class dominates >99% (model failure or attack) - Flags low mean confidence and uniform probability distributions - Returns structured security metadata alongside predictions so the API can surface warnings to the caller. * test(security): add security suite and OWASP scanner script - tests/test_security.py: unit tests for the rate limiter, payload/bbox/file validators, sanitiser, pipeline guard input/output checks, and adversarial detection helpers. - scripts/security_scan.py: external scanner that hits a running API and probes for OWASP-style misconfigurations (missing headers, unauthenticated POSTs, bbox over-area, oversized payloads). Outputs a JSON report. * docs(data): pipeline overview, band contract, and smoke tests (#33) * docs(team): add Adeolu role specification Codifies Data Pipeline & GIS Lead responsibilities: real GEE tile downloads, analysis-specific band mapping, SCL cloud masking at inference time, and the synthetic-fallback guardrail. * docs(data): document data pipeline modules and band contract Single page covering each file in the data package, the analysis-type band contract, the SCL cloud-masking rules, and the synthetic-fallback metadata convention. Helps new contributors avoid hardcoding band lists. * test(data): add band mapping smoke tests Verifies the analysis-type → band contract holds: - Sentinel-2 13-band canonical order - Per-analysis band counts (4/4/3 for deforestation/ice/flood) - SCL append-without-duplicate invariant - Band index resolution and rejection of unknown bands - Enabled vs disabled analysis types from config.yaml * feat(governance): add regional bias audit framework for model fairness (#30) - Add BiasAuditor class with demographic parity, equalized odds, and predictive parity metrics - Implement run_bias_audit() for evaluating model fairness across regions - Add check_fairness_gate() for CI/CD integration - Create scripts/audit_model.py CLI tool for running audits - Add notebooks/07_bias_audit.ipynb with visualization examples - Support Amazon, Congo, Southeast Asia, and Boreal forest regions Closes #23 Co-authored-by: Linda Oraegbunam <obielinda@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat(governance): add calibration metrics for segmentation confidence ECE, MCE, Brier score, and reliability-bin computation for binary segmentation outputs. Threshold-driven NGO alerts depend on calibrated confidence: a model that says 0.9 should be right 90% of the time, and miscalibration translates directly into missed events or false alarms. The CalibrationReport dataclass slots into the existing model card generator and release CI gate. Pure numpy at evaluation time, no torch. - ReliabilityBin / CalibrationReport dataclasses with JSON serialisation - evaluate_calibration() one-shot entrypoint - write_calibration_report() for persistence alongside model cards - 12 tests covering perfect/overconfident calibration, edge cases, input validation, and round-trip JSON * feat(governance): add Gebru-style datasheets for training datasets Companion to the Mitchell-style model card generator (#37): where a model card describes the model, a datasheet describes the dataset that trained it (Gebru et al., 2018, "Datasheets for Datasets"). Both artifacts now ship with every release. The public API mirrors model_card.py (build / render / write / generate) so contributors only learn one pattern and the release CI pipeline calls them in sequence. Sections covered: motivation, composition, collection process, preprocessing, uses (intended + inappropriate), distribution, maintenance. A REQUIRED_QUESTIONS schema enforces the bare minimum a release datasheet must answer. - Datasheet dataclass with JSON serialisation - build_datasheet() / write_datasheet() / generate() / render_markdown() - scripts/generate_datasheet.py CLI wired the same way as the model card CLI for the release CI pipeline - 12 tests covering build, validation, defaults, markdown rendering, JSON round-trip, and YAML manifest loading * fix(governance): correct _PROJECT_ROOT parents index in datasheet.py --------- Co-authored-by: Gold okpa <Okpagold@gmail.com> Co-authored-by: Linda Oraegbunam <obielinda@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Francis Umo <francisumoh360@yahoo.com> Co-authored-by: Francis Umo <franchaise@users.noreply.github.com> Co-authored-by: Olufemi Taiwo <117665354+femi23@users.noreply.github.com> Co-authored-by: Linda Oraegbunam <108290852+obielin@users.noreply.github.com> Co-authored-by: Oshgig <nifemi996@gmail.com> Co-authored-by: Hopelynconsult <Hopelynconsult@users.noreply.github.com>
1 parent e6d0248 commit d339e88

4 files changed

Lines changed: 432 additions & 0 deletions

File tree

scripts/generate_datasheet.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python
2+
"""
3+
Generate a Datasheet for a ClimateVision training dataset.
4+
5+
Usage:
6+
python scripts/generate_datasheet.py \\
7+
--manifest data/manifests/sentinel2-deforestation.yaml \\
8+
--output-dir outputs/datasheets/
9+
10+
Runs inside the release CI pipeline so every dataset version published
11+
ships with a Gebru-style datasheet alongside its model cards.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import argparse
17+
import logging
18+
import sys
19+
from pathlib import Path
20+
21+
from climatevision.governance.datasheet import generate
22+
23+
logger = logging.getLogger("generate_datasheet")
24+
25+
26+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
27+
parser = argparse.ArgumentParser(description=__doc__)
28+
parser.add_argument("--manifest", type=Path, required=True, help="Dataset manifest (yaml/json)")
29+
parser.add_argument("--output-dir", type=Path, default=None, help="Where to write the datasheet")
30+
parser.add_argument("--name", default=None, help="Override dataset name")
31+
parser.add_argument("--version", default=None, help="Override dataset version")
32+
parser.add_argument("-v", "--verbose", action="store_true")
33+
return parser.parse_args(argv)
34+
35+
36+
def main(argv: list[str] | None = None) -> int:
37+
args = parse_args(argv)
38+
logging.basicConfig(
39+
level=logging.DEBUG if args.verbose else logging.INFO,
40+
format="%(asctime)s %(levelname)s %(name)s %(message)s",
41+
)
42+
43+
paths = generate(
44+
manifest=args.manifest,
45+
output_dir=args.output_dir,
46+
name=args.name,
47+
version=args.version,
48+
)
49+
for label, path in paths.items():
50+
print(f"{label}: {path}")
51+
return 0
52+
53+
54+
if __name__ == "__main__":
55+
sys.exit(main())

src/climatevision/governance/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Calibration metrics for confidence reliability
88
- Anomaly detection for inference inputs/outputs
99
- Model audit trails and version tracking
10+
- Datasheets for training datasets (Gebru et al., 2018)
1011
"""
1112

1213
from .explainability import (
@@ -53,6 +54,13 @@
5354
reliability_bins,
5455
write_calibration_report,
5556
)
57+
from .datasheet import (
58+
Datasheet,
59+
build_datasheet,
60+
generate as generate_datasheet,
61+
render_markdown as render_datasheet_markdown,
62+
write_datasheet,
63+
)
5664

5765
__all__ = [
5866
# Explainability
@@ -93,4 +101,10 @@
93101
"maximum_calibration_error",
94102
"reliability_bins",
95103
"write_calibration_report",
104+
# Datasheet
105+
"Datasheet",
106+
"build_datasheet",
107+
"generate_datasheet",
108+
"render_datasheet_markdown",
109+
"write_datasheet",
96110
]
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
Datasheets for the datasets that train ClimateVision models.
3+
4+
Companion to the Mitchell-style model cards in ``governance.model_card``:
5+
where a model card describes the *model*, a datasheet describes the
6+
*dataset* the model was trained on (Gebru et al., 2018, "Datasheets for
7+
Datasets"). The two artifacts answer different questions and both need
8+
to ship with a release.
9+
10+
The module mirrors the model_card public surface (``build``, ``render``,
11+
``write``, ``generate``) so contributors only have to learn one pattern,
12+
and the release CI pipeline can call them in sequence.
13+
14+
Sections covered:
15+
16+
- Motivation
17+
- Composition
18+
- Collection process
19+
- Preprocessing, cleaning, labeling
20+
- Uses (intended and inappropriate)
21+
- Distribution
22+
- Maintenance
23+
24+
Every section is a free-form ``dict`` of question -> answer so the schema
25+
can grow without code changes; ``REQUIRED_QUESTIONS`` enforces the bare
26+
minimum a release datasheet must answer.
27+
"""
28+
29+
from __future__ import annotations
30+
31+
import json
32+
import logging
33+
from dataclasses import dataclass, field
34+
from datetime import datetime, timezone
35+
from pathlib import Path
36+
from typing import Any, Optional, Union
37+
38+
logger = logging.getLogger(__name__)
39+
40+
_PROJECT_ROOT = Path(__file__).resolve().parents[3]
41+
_DEFAULT_OUTPUT_DIR = _PROJECT_ROOT / "outputs" / "datasheets"
42+
43+
REQUIRED_QUESTIONS = {
44+
"motivation": ("purpose", "creators"),
45+
"composition": ("instances", "labels", "splits"),
46+
"collection_process": ("source", "timeframe"),
47+
"uses": ("intended_uses", "inappropriate_uses"),
48+
}
49+
50+
51+
@dataclass
52+
class Datasheet:
53+
"""Structured datasheet for a single training dataset."""
54+
55+
name: str
56+
version: str
57+
motivation: dict
58+
composition: dict
59+
collection_process: dict
60+
preprocessing: dict
61+
uses: dict
62+
distribution: dict
63+
maintenance: dict
64+
generated_at: str = field(
65+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
66+
)
67+
68+
def to_dict(self) -> dict:
69+
return {
70+
"name": self.name,
71+
"version": self.version,
72+
"motivation": self.motivation,
73+
"composition": self.composition,
74+
"collection_process": self.collection_process,
75+
"preprocessing": self.preprocessing,
76+
"uses": self.uses,
77+
"distribution": self.distribution,
78+
"maintenance": self.maintenance,
79+
"generated_at": self.generated_at,
80+
}
81+
82+
83+
_DEFAULT_INAPPROPRIATE_USES = [
84+
"Training models for real-time legal enforcement against individual landowners.",
85+
"Land-rights or sovereignty disputes without on-the-ground verification.",
86+
"Generative model training where label provenance is required to be human-verified.",
87+
]
88+
89+
_DEFAULT_MAINTENANCE = {
90+
"owner": "ClimateVision Governance <governance@climate-vision.org>",
91+
"update_cadence": "Reviewed each minor release; refreshed when source providers change.",
92+
"deprecation_policy": (
93+
"Versions are retained for two minor releases after supersession; "
94+
"models trained on deprecated versions are flagged in their model cards."
95+
),
96+
}
97+
98+
99+
def _coerce_config(config: Union[dict, str, Path]) -> dict:
100+
if isinstance(config, dict):
101+
return config
102+
path = Path(config)
103+
text = path.read_text()
104+
if path.suffix in {".yml", ".yaml"}:
105+
try:
106+
import yaml
107+
except ImportError as exc: # pragma: no cover - import guard
108+
raise RuntimeError("PyYAML is required to load YAML configs") from exc
109+
return yaml.safe_load(text)
110+
return json.loads(text)
111+
112+
113+
def _validate(datasheet: "Datasheet") -> None:
114+
missing: list[str] = []
115+
for section_name, required_keys in REQUIRED_QUESTIONS.items():
116+
section = getattr(datasheet, section_name)
117+
for key in required_keys:
118+
if key not in section or section[key] in (None, "", []):
119+
missing.append(f"{section_name}.{key}")
120+
if missing:
121+
raise ValueError(f"datasheet missing required answers: {missing}")
122+
123+
124+
def build_datasheet(
125+
manifest: Union[dict, str, Path],
126+
*,
127+
name: Optional[str] = None,
128+
version: Optional[str] = None,
129+
) -> Datasheet:
130+
"""Build a Datasheet from a structured dataset manifest."""
131+
m = _coerce_config(manifest)
132+
133+
resolved_name = name or m.get("name") or "climatevision-dataset"
134+
resolved_version = version or m.get("version") or "0.0.0"
135+
136+
uses = dict(m.get("uses", {}))
137+
uses.setdefault("inappropriate_uses", list(_DEFAULT_INAPPROPRIATE_USES))
138+
139+
sheet = Datasheet(
140+
name=resolved_name,
141+
version=resolved_version,
142+
motivation=dict(m.get("motivation", {})),
143+
composition=dict(m.get("composition", {})),
144+
collection_process=dict(m.get("collection_process", {})),
145+
preprocessing=dict(m.get("preprocessing", {})),
146+
uses=uses,
147+
distribution=dict(m.get("distribution", {})),
148+
maintenance=dict(m.get("maintenance", _DEFAULT_MAINTENANCE)),
149+
)
150+
_validate(sheet)
151+
return sheet
152+
153+
154+
def _render_section(title: str, body: dict) -> list[str]:
155+
if not body:
156+
return [f"## {title}", "_Not documented._", ""]
157+
lines = [f"## {title}"]
158+
for key, value in body.items():
159+
pretty_key = key.replace("_", " ").title()
160+
if isinstance(value, list):
161+
lines.append(f"### {pretty_key}")
162+
lines.extend(f"- {item}" for item in value)
163+
elif isinstance(value, dict):
164+
lines.append(f"### {pretty_key}")
165+
lines.append(f"```json\n{json.dumps(value, indent=2)}\n```")
166+
else:
167+
lines.append(f"- **{pretty_key}**: {value}")
168+
lines.append("")
169+
return lines
170+
171+
172+
def render_markdown(sheet: Datasheet) -> str:
173+
sections = [
174+
f"# Datasheet: {sheet.name} ({sheet.version})",
175+
f"_Generated {sheet.generated_at}_",
176+
"",
177+
"_Format: Gebru et al., 2018, \"Datasheets for Datasets\"._",
178+
"",
179+
]
180+
sections += _render_section("Motivation", sheet.motivation)
181+
sections += _render_section("Composition", sheet.composition)
182+
sections += _render_section("Collection Process", sheet.collection_process)
183+
sections += _render_section("Preprocessing, Cleaning, Labeling", sheet.preprocessing)
184+
sections += _render_section("Uses", sheet.uses)
185+
sections += _render_section("Distribution", sheet.distribution)
186+
sections += _render_section("Maintenance", sheet.maintenance)
187+
return "\n".join(sections) + "\n"
188+
189+
190+
def write_datasheet(
191+
sheet: Datasheet,
192+
output_dir: Optional[Union[str, Path]] = None,
193+
) -> dict[str, Path]:
194+
output_dir = Path(output_dir) if output_dir else _DEFAULT_OUTPUT_DIR
195+
output_dir.mkdir(parents=True, exist_ok=True)
196+
197+
base = f"{sheet.name}_{sheet.version}"
198+
md_path = output_dir / f"{base}.md"
199+
json_path = output_dir / f"{base}.json"
200+
201+
md_path.write_text(render_markdown(sheet))
202+
json_path.write_text(json.dumps(sheet.to_dict(), indent=2))
203+
204+
logger.info("Wrote datasheet to %s and %s", md_path, json_path)
205+
return {"markdown": md_path, "json": json_path}
206+
207+
208+
def generate(
209+
manifest: Union[dict, str, Path],
210+
output_dir: Optional[Union[str, Path]] = None,
211+
**kwargs: Any,
212+
) -> dict[str, Path]:
213+
"""End-to-end: load manifest, build the datasheet, render to disk."""
214+
sheet = build_datasheet(manifest, **kwargs)
215+
return write_datasheet(sheet, output_dir=output_dir)

0 commit comments

Comments
 (0)