food_security¶
Vegetation and climate stress assessment for food-security risk. Entry
point: FoodSecurityUseCase.
Use case¶
use_case ¶
FoodSecurityUseCase ¶
Bases: BaseUseCase
Entry point for the food security assessment domain.
Minimal config (all optional — defaults match Marsabit County, Kenya 2018–2023):
{ "aoi_geojson": {"type": "Polygon", "coordinates": [...]}, "gee_project": "your-gee-project-id", "start_date": "2018-01-01", "end_date": "2023-12-31", "lt_baseline_start": "2001-01-01", # long-term baseline for VCI/TCI "s2_start": "2020-01-01", # Sentinel-2 MNDWI start "model_type": "rf", # "rf" | "xgboost" | "ensemble" "n_pixels": 3000, "scale": 1000, # metres — MODIS / CHIRPS native "output_dir": "outputs", "prefix": "food_security", }
Source code in food_security/use_case.py
fetch_data ¶
Authenticate GEE and parse the AOI geometry (see _fetch_from_dict).
Source code in food_security/use_case.py
preprocess ¶
Build the feature stack and sample training pixels (see _preprocess_raw).
run_model ¶
Train models, export the risk COG, and package an AnalysisOutput.
Source code in food_security/use_case.py
run ¶
Single AOI analysis — full pipeline in one call.
Source code in food_security/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_vci_tci ¶
fetch_vci_tci(aoi: Geometry, lt_start: str, study_start: str, study_end: str, scale: int = 1000) -> xr.Dataset
Compute VCI and TCI from MODIS NDVI and LST relative to the long-term baseline.
VCI = (NDVI_current − NDVI_min) / (NDVI_max − NDVI_min) × 100 TCI = (LST_max − LST_current) / (LST_max − LST_min) × 100
Returns Dataset with variables 'vci' and 'tci'.
Source code in food_security/features.py
fetch_rainfall_anomaly_pct ¶
fetch_rainfall_anomaly_pct(aoi: Geometry, lt_start: str, study_start: str, study_end: str, scale: int = 1000) -> xr.Dataset
Pixel-wise CHIRPS rainfall anomaly as percentage vs long-term daily mean. anomaly_pct = (study_mean − LT_mean) / (LT_mean + ε) × 100
Returns Dataset with variable 'rainfall_anom_pct'.
Source code in food_security/features.py
fetch_ndvi_slope_img ¶
Pixel-wise MODIS NDVI linear trend (NDVI per year) from lt_start to study_end. Returns Dataset with variable 'ndvi_slope'.
Source code in food_security/features.py
fetch_mndwi ¶
Sentinel-2 MNDWI = (Green − SWIR1) / (Green + SWIR1) median composite. Returns Dataset with variable 'mndwi'.
Source code in food_security/features.py
fetch_terrain_slope ¶
SRTM-derived terrain slope in degrees. Returns Dataset with variable 'slope_terrain'.
Source code in food_security/features.py
fetch_landcover ¶
ESA WorldCover v200 normalised to [0, 1] (class value / 100). Returns Dataset with variable 'land_cover'.
Source code in food_security/features.py
build_feature_datasets ¶
Download all seven food-security feature bands from GEE in parallel. The six fetch calls are independent HTTP requests; they run concurrently via DaskEngine.run_io_parallel (ThreadPoolExecutor) sharing the GEE session. Returns a dict keyed by band group name, consumed by cog_export.
Source code in food_security/features.py
build_gee_feature_stack ¶
Assemble the 7-band GEE image used for stratified pixel sampling. Band order matches FEATURE_COLS.
Source code in food_security/features.py
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 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 450 451 452 453 454 455 456 | |
fetch_ndvi_monthly_timeseries ¶
Monthly area-mean MODIS NDVI over [start, end]. Returns DataFrame with DatetimeIndex and column 'ndvi'.
Source code in food_security/features.py
fetch_monthly_rainfall ¶
Monthly area-mean CHIRPS rainfall sum over [start, end]. Returns DataFrame with DatetimeIndex and column 'rain_mm'.
Source code in food_security/features.py
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 3-class food insecurity labels. Labels: fixed absolute thresholds on the composite food stress score (0-100 scale). 0 = Low Risk (score < 33) 1 = Medium Risk (33 <= score < 66) 2 = High Risk (score >= 66) Returns DataFrame with FEATURE_COLS + ['food_score', 'label'].
Source code in food_security/features.py
align_datasets ¶
align_datasets(datasets: dict[str, Dataset], ref_key: str = 'vci_tci', method_continuous: InterpOptions = 'linear', method_categorical: InterpOptions = 'nearest') -> dict[str, xr.Dataset]
Interpolate all datasets onto the VCI/TCI 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 food_security/features.py
Model¶
model ¶
FoodSecurityModel ¶
Orchestrates the full ML pipeline for a single food security analysis. config['model_type'] selects which model(s) are trained and which predictions drive the primary risk distribution: "rf" — Random Forest only (default) "xgboost" — XGBoost only "ensemble" — both, combined as mean softmax probabilities of RF + XGBoost Only the selected model type is trained. Trained models and scaler are stored on self for use by cog_export (whichever model wasn't needed stays None).
Source code in food_security/model.py
predict ¶
predict(df: DataFrame, ndvi_df: DataFrame | None = None, rain_df: DataFrame | None = None, config: dict | None = None) -> dict
Parameters¶
df : DataFrame with FEATURE_COLS + ['food_score', 'label'] ndvi_df : monthly NDVI DataFrame (date index, 'ndvi' column) rain_df : monthly rainfall DataFrame (date index, 'rain_mm' column) config : optional dict; reads 'model_type' (default 'rf')
Returns¶
dict with keys: stats, charts
Source code in food_security/model.py
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 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 | |
train_rf ¶
train_rf(X_train: ndarray, y_train: ndarray, cv_folds: int = 5) -> tuple[RandomForestClassifier, dict]
Fit a balanced Random Forest classifier. Returns (model, metadata).
Source code in food_security/model.py
train_xgb ¶
Fit XGBoost multi-class classifier via the low-level Booster API with sample weights.
Uses xgboost.train() rather than the XGBClassifier sklearn wrapper because the wrapper requires every class in FOOD_CLASSES to appear in y_train (it infers num_class from np.unique(y_train) and rejects gaps). Some AOIs/time windows genuinely have zero samples of a given risk class, which is a legitimate state, not invalid input, so class count is fixed via XGB_PARAMS instead.
Returns (booster, metadata).
Source code in food_security/model.py
pad_rf_proba ¶
Expand RF's predict_proba output to the full FOOD_CLASSES width.
RandomForestClassifier only emits a column per class it actually saw during fit (via rf.classes_), whereas train_xgb's Booster always outputs len(FOOD_CLASSES) columns regardless of what the AOI's data contained. Without padding, an AOI missing one risk class produces mismatched shapes (e.g. (n, 2) vs (n, 3)) the moment RF and XGBoost probabilities are combined for the ensemble.
Source code in food_security/model.py
evaluate_models ¶
evaluate_models(rf: RandomForestClassifier | None, xgb: Booster | None, X_test: ndarray, y_test: ndarray) -> dict
Evaluate whichever of RF/XGBoost were actually trained (both, for an "ensemble" selection; exactly one otherwise) on the held-out test set, plus their mean-proba ensemble when both are present.
Source code in food_security/model.py
compute_shap_importance ¶
TreeExplainer SHAP values for whichever model was actually selected/trained — see core.shap_utils for the axis-handling details.
Source code in food_security/model.py
build_food_security_charts ¶
build_food_security_charts(eval_result: dict, shap_payload: dict, ndvi_df: DataFrame, rain_df: DataFrame, vci_mean: float, tci_mean: float, vhi_mean: float, model_type: str = 'rf') -> dict
Assemble frontend-ready chart payloads for the food security module.
Source code in food_security/model.py
COG export¶
cog_export ¶
predict_food_security_grid ¶
predict_food_security_grid(rf_model: RandomForestClassifier | None, xgb_model: Booster | None, scaler: StandardScaler, datasets: dict[str, Dataset], model_type: str = 'rf', aoi_geojson: dict | None = None) -> dict
Run full-grid inference over the aligned feature datasets and classify every valid pixel into Low/Medium/High food-insecurity risk.
Returns a dict with the classified risk_grid (int-encoded per
RISK_INT, 0 = nodata), the raster transform/crs/shape, and
per-class pixel counts/percentages.
Water bodies and built-up land cover are masked out before classification.
Used both by export_food_security_cog (COG write) and the use case's
GeoJSON conversion.
Source code in food_security/cog_export.py
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 | |
export_food_security_cog ¶
export_food_security_cog(rf_model: RandomForestClassifier | None, xgb_model: Booster | None, scaler: StandardScaler, datasets: dict[str, Dataset], output_dir: str, prefix: str, model_type: str = 'rf', aoi_geojson: dict | None = None) -> dict[str, str]
Build the full-AOI feature matrix from in-memory xarray Datasets, run inference with the selected model, classify into 3 food insecurity risk classes, and write a Cloud Optimised GeoTIFF.
Parameters¶
rf_model : trained RandomForestClassifier xgb_model : trained XGBoost Booster scaler : fitted StandardScaler (from FoodSecurityModel.scaler) datasets : dict returned by build_feature_datasets output_dir : local directory for COG output prefix : file prefix model_type : "rf" | "xgboost" | "ensemble" aoi_geojson : optional AOI polygon used to mask pixels outside the true AOI
Returns¶
dict with key 'food_security_risk' → path string