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
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 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 | |
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. Always trains Random Forest + XGBoost. config['model_type'] selects which predictions drive the primary risk distribution: "rf" — Random Forest (default) "xgboost" — XGBoost "ensemble" — mean softmax probabilities of RF + XGBoost Trained models and scaler are stored on self for use by cog_export.
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
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 425 426 | |
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 RF, XGBoost, and mean-proba ensemble on the held-out test set.
Source code in food_security/model.py
compute_shap_importance ¶
TreeExplainer SHAP on XGBoost (always used for SHAP regardless of model_type). Returns features sorted by descending mean |SHAP| averaged across all classes.
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, xgb_model: Booster, 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 | |
export_food_security_cog ¶
export_food_security_cog(rf_model: RandomForestClassifier, xgb_model: Booster, 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