← All projects

Case study · Data Science & Machine Learning

Predicting district-level maize yields in Ghana

An end-to-end pipeline predicting district-level maize yields in Ghana. It combines yield records with growing-season weather from the NASA POWER API, soil types and policy/pest indicators, then trains a Random Forest + Gradient Boosting ensemble.

Role
Solo: data, modelling, evaluation
Model
Random Forest + Gradient Boosting voting ensemble
Data
1,681 district-year yield records (2010–2016) + NASA POWER weather
Stack
Python, Pandas, scikit-learn, geopy, NASA POWER API, joblib
R² on a random held-out 20% split
0.95
Input features
10

The problem

Maize is one of Ghana’s most important staple crops, and yields vary sharply between districts and seasons. Agencies and farmers plan inputs, storage and food-security interventions with little forward-looking information. The goal was a model that estimates a district’s yield for a season from conditions that can be observed in advance.

Data

The project joins three sources. The first is district-level maize yield records for 2010–2016. The second is daily weather for each district from the NASA POWER agro-climatology API: rainfall, temperature, humidity, solar radiation and topsoil moisture. The third is dominant soil type, mapped from each district’s region.

  • District names were inconsistent across sources (“Municipal”, “Metro”, bracketed suffixes, hyphens), so I wrote a normaliser to make the joins reliable.
  • Each district was geocoded with geopy, and the coordinates were used to query NASA POWER for 2010–2021.
  • Daily weather was aggregated over the April–August growing season: total rainfall, plus mean temperature, humidity, sunlight and soil moisture.

Feature engineering

  • Previous-year yield (lag-1) per district, the strongest single signal of local productivity.
  • Pest-outbreak indicator for the 2016–2017 seasons.
  • Policy indicator for the Planting for Food and Jobs programme (2017 onward).
  • Outlier capping: yields above 4 t/ha were winsorised to limit the influence of reporting errors.
  • Soil type is one-hot encoded inside a scikit-learn ColumnTransformer, so preprocessing travels with the model.

Modelling

I trained a voting ensemble of a Random Forest and a Gradient Boosting regressor (200 trees each). Averaging the two balances the low variance of bagging against the low bias of boosting. Preprocessing and model live in a single Pipeline, saved with joblib, so the same object can be loaded by an app or API without re-implementing the feature logic.

Limitations & what I’d do next

Being explicit about evaluation matters more to me than the headline number. The current R² comes from a random 80/20 split, and two choices in the pipeline make that score optimistic:

  • District records for 2017–2021 were not available, so those years were filled with national average yields. The model can predict those rows almost perfectly, which inflates R².
  • With a lag feature, a random split lets the model train on later seasons and test on earlier ones from the same district.
  • Next: a time-based split (train on earlier seasons, test on the latest), a comparison against a naive “same as last year” baseline, real post-2016 district data, and SHAP feature attributions.
  • Then: serve the saved pipeline in a small Streamlit app where a user picks a district and adjusts rainfall to see the predicted yield.