land_degradation¶
Vegetation decline and rangeland degradation detection. Entry point:
LandDegradationUseCase.
Use case¶
use_case ¶
LandDegradationUseCase ¶
Bases: BaseUseCase
Entry point for the land degradation domain.
Minimal config (all optional — defaults match Northern Burkina Faso 2015–2024):
{ "aoi_geojson": {"type": "Polygon", "coordinates": [...]}, "gee_project": "your-gee-project-id", "start_date": "2015-01-01", "end_date": "2024-12-31", "model_type": "lgbm", # "rf" | "lgbm" | "ensemble" "n_pixels": 3000, "scale": 1000, # metres — uniform for all feature rasters "output_dir": "outputs", "prefix": "land_degradation", }
Source code in land_degradation/use_case.py
fetch_data ¶
Authenticate GEE and parse the AOI geometry (see _fetch_from_dict).
Source code in land_degradation/use_case.py
preprocess ¶
Build the feature stack and sample training pixels (see _preprocess_raw).
run_model ¶
Train models, export the degradation COG, and package an AnalysisOutput.
Source code in land_degradation/use_case.py
run ¶
Single AOI analysis — full pipeline in one call.
Source code in land_degradation/use_case.py
run_date_ranges ¶
Run the same AOI over multiple date-range configurations in parallel.
run_multi_regions ¶
Run multiple AOI configs (same timeframe, different regions) in parallel.
Features¶
features ¶
fetch_ndvi_stack ¶
Download MODIS MOD13A3 pixel-wise NDVI trend at scale metres.
Returns Dataset with variables ndvi_slope, ndvi_mean, ndvi_cv.
Source code in land_degradation/features.py
fetch_ndvi_timeseries ¶
Fetch area-averaged monthly MODIS NDVI and return as an annual pandas Series. Index = integer years; values = annual mean NDVI.
Source code in land_degradation/features.py
fetch_s2_indices ¶
Compute annual Sentinel-2 BSI and NDTI composites (cloud < 30 %). Downloads all years as a single multi-band GeoTIFF and returns the temporal mean as a 2-variable Dataset with bsi and ndti.
Source code in land_degradation/features.py
fetch_terrain_slope ¶
Download SRTM-derived slope at scale metres.
Source code in land_degradation/features.py
fetch_rainfall_anomaly ¶
Pixel-wise CHIRPS precipitation linear trend (mm yr⁻¹) over [start, end]. Returns Dataset with variable rainfall_anom.
Source code in land_degradation/features.py
fetch_landcover ¶
Download ESA WorldCover 2021. Returns Dataset with variable Map.
Source code in land_degradation/features.py
build_feature_datasets ¶
Download all feature bands from GEE in parallel. The five fetch calls are independent HTTP requests; they run concurrently via DaskEngine.run_io_parallel (ThreadPoolExecutor) sharing the GEE session. Keyed by band group; consumed by both sample_training_data and cog_export.
Source code in land_degradation/features.py
build_gee_feature_stack ¶
Assemble the 8-band GEE image used for stratified pixel sampling. Uses the same band order as FEATURE_COLS.
Source code in land_degradation/features.py
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | |
sample_training_data ¶
sample_training_data(feature_stack: Image, aoi: Geometry, n_pixels: int = 3000, scale: int = 1000, seed: int = 42) -> pd.DataFrame
Sample n_pixels from the GEE feature stack and assign binary degradation labels. Labels: composite score >= DEGRADED_SCORE_THRESHOLD → 1 (Degraded), remainder → 0 (Not Degraded). Returns DataFrame with FEATURE_COLS + ['deg_score', 'deg_class'].
Source code in land_degradation/features.py
align_datasets ¶
align_datasets(datasets: dict[str, Dataset], ref_key: str = 'ndvi', method_continuous: InterpOptions = 'linear', method_categorical: InterpOptions = 'nearest') -> dict[str, xr.Dataset]
Interpolate all datasets onto the NDVI reference grid. Land cover is treated as categorical (nearest-neighbour). population_count is passed through entirely unchanged, at its own native ~100 m resolution — it is never interpolated onto this (coarser) reference grid. Summing population within a risk zone requires the opposite: upsampling the (categorical) classification onto population's native grid — see core.population.population_exposure, which cog_export.py uses for this instead of relying on align_datasets. Interpolating population here would silently corrupt totals (verified against live GEE data — see core.population.fetch_population_count's docstring).
Each dataset is chunked before interpolation so that xarray produces Dask-backed lazy arrays; dask.compute() then materialises them all in a single parallel scheduler pass.
Source code in land_degradation/features.py
Model¶
model ¶
FeatureNamedLGBMClassifier ¶
Bases: LGBMClassifier
LightGBM classifier that preserves feature names for array inputs.
LandDegradationModel ¶
Orchestrates the full ML pipeline for a single land degradation analysis. Always trains RF + LightGBM. config['model_type'] selects which predictions drive the primary risk distribution: "rf" — Random Forest "lgbm" — LightGBM (default) "ensemble" — majority vote of RF + LightGBM Trained models and scaler are stored on self for use by cog_export.
Source code in land_degradation/model.py
predict ¶
Parameters¶
df : DataFrame with FEATURE_COLS + ['deg_score', 'deg_class'] ndvi_annual : Annual mean NDVI Series (index = int years) config : optional dict; reads 'model_type' (default 'lgbm') and 'scale'
Returns¶
dict with keys: stats, charts
Source code in land_degradation/model.py
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | |
train_rf ¶
train_rf(X_train: ndarray, y_train: ndarray, cv_folds: int = 5) -> tuple[RandomForestClassifier, dict]
Fit a balanced Random Forest and report CV weighted F1. Returns (model, metadata).
Source code in land_degradation/model.py
train_lgbm ¶
train_lgbm(X_train: ndarray, y_train: ndarray, cv_folds: int = 5) -> tuple[lgb.LGBMClassifier, dict]
Fit a balanced LightGBM classifier and report CV weighted F1. Returns (model, metadata).
Source code in land_degradation/model.py
evaluate_models ¶
evaluate_models(rf: RandomForestClassifier, lgbm: LGBMClassifier, X_test: ndarray, y_test: ndarray) -> dict
Evaluate RF, LightGBM, and majority-vote ensemble on the held-out test set.
Source code in land_degradation/model.py
compute_shap_importance ¶
TreeExplainer SHAP values sorted by descending mean |SHAP|. For multi-output (RF), averages across classes.
Source code in land_degradation/model.py
compute_ndvi_trend ¶
OLS linear regression + Mann-Kendall test + Binseg RBF breakpoint detection on an annual NDVI series (index = integer years). Returns a flat dict of trend statistics for inclusion in the result payload.
Source code in land_degradation/model.py
build_degradation_charts ¶
build_degradation_charts(eval_result: dict, shap_payload: dict, ndvi_annual: Series, trend_stats: dict, model_type: str = 'lgbm', scale: int = 1000) -> dict
Assemble frontend-ready chart payloads matching LandDegradationUseCase.run() schema.
Source code in land_degradation/model.py
COG export¶
cog_export ¶
export_degradation_cog ¶
export_degradation_cog(rf_model: RandomForestClassifier, lgbm_model: LGBMClassifier, scaler: StandardScaler, datasets: dict[str, Dataset], output_dir: str = 'outputs', prefix: str = 'land_degradation', model_type: str = 'lgbm', aoi_geojson: dict | None = None, scale: int = 1000) -> DegradationCogResult
Apply the trained model to the full pixel grid and write a Cloud-Optimised GeoTIFF.
Prediction values
0 = Not Degraded 1 = Degraded -1 = NoData (pixels with missing feature values)
Returns dict with keys
'degradation_risk' → path string 'risk_pct' → [not_degraded_pct, degraded_pct] over the AOI's valid (non-nodata) pixels, matching DEGRADATION_CLASSES order. 'risk_ha' → same two classes in hectares (pixel area = scale²/10_000)
This reflects every pixel actually painted on the map, unlike the training-sample-based stats LandDegradationModel.predict() computes, which only covers the held-out test split and can diverge sharply from the full-AOI distribution.
Source code in land_degradation/cog_export.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 | |