BTBalbino
HomeProjectsBlogResearchCertificationsAboutContact
BTBalbino Tchoutzine

Computer Engineering student at ENSPY, building applied AI for development across computer vision, geospatial ML, and low-resource NLP, with a particular focus on Africa

Navigation

BlogResearchCertificationsAboutContact

Contact

GitHubLinkedInX (Twitter)tchoutzine@gmail.com
CV FRCV EN

© 2026 Balbino Tchoutzine. All rights reserved.

Built with Next.js & Vercel

Predicting Chad's crop yields from satellite data | 1st place (private LB) in a Zindi challenge
← Back to blog
August 21, 2026·8 min readAIMachine Learning

Predicting Chad's crop yields from satellite data | 1st place (private LB) in a Zindi challenge

New Zindi challenge, new obsession for a few weeks. This one was about a topic that speaks to me directly: predicting agricultural yields in Africa from satellite data, for Chad.

Under the handle Zoom387, I submitted my solution on August 15, 2026. Public score: wMAPE of 0.19848, 3rd place on the public leaderboard. On the private leaderboard, the one that actually counts: 1st place.

AI prediction of agricultural yields from remote sensing and time series, Zindi challenge for the Chad Institute of Algorithms

The challenge: predicting yields that don't exist yet

The task was simple to state, hard to execute: predict yield (Rendement_kg_ha) by province, crop, and agricultural campaign, from satellite-derived climate and vegetation indicators plus historical yields.

Three obstacles made it genuinely difficult:

  • Sparse data: only about 1,800 training rows.
  • Predicting the future: the test years (2024/2025 and 2025/2026 campaigns) have no known cultivated area or production, only climate indicators.
  • Cold starts: the crop Fonio and the province Ennedi Est only appear in the test set, never in training. There's no history to lean on for either.

The official metric, wMAPE, weights errors by cultivated area:

wMAPE = Σ(area_i × |yield_i − prediction_i|)
        ─────────────────────────────────
              Σ(area_i × yield_i)

Getting a large farm wrong costs more than getting a small plot wrong. That's why the training target (see below) is weighted by area too: the model learns to directly minimize the right error, not a proxy for it.

Building features with zero external data

The challenge rules required working only from the files Zindi provided (train.csv, test.csv): no FAO, NASA POWER, FAOSTAT, or any other external dataset was allowed. In practice that meant every climate or vegetation signal had to already exist in the provided columns, and everything else (trends, cold starts, aggregates) had to be reconstructed from the yield history sitting in train.csv alone. No falling back on longer or more reliable external climate series to patch the gaps.

The feature families I built, and what each one contributes:

  • Yield history (province x crop): lags 1 to 3, rolling mean/std/median, linear trend. This is the strongest signal in the problem: a crop's yield in a given province is heavily autocorrelated from one campaign to the next. The lags give the recent level, the rolling std captures volatility (an erratic crop needs to be predicted more cautiously), and the linear trend picks up slow dynamics (improving practices, soil degradation) that the lags alone miss.
  • Aggregates: statistics by crop, by province, by zone x crop, by province x crop. These act as a statistical safety net: when the history for a specific province x crop pair is short or noisy, the model can fall back on averages at a broader level instead of overfitting to three or four data points.
  • Climate: current-year vegetation index (wVHI), temperature, precipitation, humidity, plus anomalies relative to the province's historical average. This is the only signal available for the test years, which have no known area or production. Anomalies (deviation from the local norm) matter more than raw values: a dry season hits differently depending on whether a province is normally arid or humid.
  • Cold-start proxies: Fonio inherits the behavior of related cereals (millet, sorghum) by province and year; Ennedi Est inherits from neighboring Sahelian provinces (Kanem, Barh El Ghazal, Wadi Fira, Batha). Without this, the model would have zero history to draw on for either case and would have to extrapolate blind.
  • Encodings: smoothed target encoding and label encoding for Province, Crop, Zone, and Category. Smoothed target encoding lets the tree models pick up the average yield level tied to each category directly, without exploding the number of splits needed compared to plain one-hot encoding on high-cardinality variables.

The training target was log1p(yield), with sample weights of log1p(area) to directly align training with the wMAPE logic.

The pipeline: five models, one stacker, sequential inference

Why five tree models instead of one well-tuned one? With only 1,800 training rows, a single model, however well optimized, stays sensitive to the noise in its own learning bias. The idea comes from the stacking literature in ML competitions (well documented across Kaggle): models that make different errors, even when individually close in score, correct each other once combined. I picked these five specifically because they don't share the same bias:

  • LightGBM and XGBoost both do gradient boosting, but with different tree-growth strategies (leaf-wise for LightGBM, historically level-wise for XGBoost), so they don't make quite the same mistakes.
  • CatBoost handles categorical variables natively with ordered boosting, which limits overfitting on Province, Crop, and Zone compared to plain target encoding fed in upstream.
  • HistGradientBoosting (scikit-learn) uses histogram-based feature binning, an approach close to LightGBM's but with its own regularization.
  • ExtraTrees breaks the boosting logic entirely: it's an ensemble of fully randomized trees, no sequential boosting, contributing raw diversity rather than fine-tuned optimization.

These five models, trained in parallel, are then stacked through a Ridge meta-learner on their out-of-fold predictions, and calibrated per crop to correct systematic bias without extrapolating.

The detail that changed everything: the test years aren't independent. Predicting 2025 needs 2024 history that doesn't exist yet at training time. The fix: predict 2024 first, feed those predictions back in as pseudo-labels, rebuild the features with that extended history, then predict 2025. Sequential inference, not a simple batch job.

Last step: instead of keeping a single random seed, the final solution takes the row-wise median of predictions from three different seeds (7, 456, 789). More robust than a mean against a seed that goes off, and it beat every alternative I tested (mean, geometric mean, more elaborate blends) on the public leaderboard.

A few bumps along the way

The final pipeline wasn't the first thing I tried. Several approaches that looked reasonable on paper actually hurt the public score:

  • A "time-safe" rewrite of the feature engineering (more rigorous in theory): wMAPE ≈ 0.287.
  • Splitting the model by crop category instead of one global model: wMAPE ≈ 0.299.
  • More aggressive shrinkage on the predictions: wMAPE ≈ 0.203.

Keeping these in the solution documentation, even after finding something better, mattered to me. It shows the actual path taken, not just the finish line.

Result

SplitScore (wMAPE)Rank
Public0.1984795213rd
Private0.2257585961st out of 27

Submission v16_3 (ID e3vJYTzM), August 15, 2026. Zindi notes this ranking is subject to change until September 5, 2026, pending final review of winning solutions, standard procedure for top finishers.

Zindi certificate: Zoom387 currently ranked 1 out of 27 in the Chad crop yield prediction challenge

Final private leaderboard for the Zindi Chad challenge, Balbino_Tchoutzine in first place with a score of 0.225758596

Reproducibility

The full solution is on GitHub: notebooks, source code, Zindi data, complete documentation (README.md, SOLUTION.md, CODE_REVIEW.md).

github.com/zoom-BT/zindi-chad-crop-yield

python generate_submission.py regenerates the exact submitted file in under 5 seconds from the archived predictions. The 02_train_from_scratch.ipynb notebook reruns the full training of all five models if you want to verify the method end to end.

Fun fact

This challenge checks exactly the boxes I care about right now: geospatial ML applied to a real problem (food security), time series with hard constraints (predicting a future with no future data), and a data-poor setting where feature engineering matters more than raw compute.

It's also my ongoing training ground as a Zindi Ambassador: 19+ challenges to date, and this is probably the most technically complete one so far. The full solution documentation (architecture, ETL, modeling choices, error handling) is written to the format Zindi asks its winners for, exactly as if it were going to be audited by a judge.

This challenge also moves my overall Zindi ranking forward:

Zindi all-time ranking in Cameroon: Zoom387 at 8th of 285

All-time ranking, Cameroon: 8th of 285

Zindi 2026 season ranking in Cameroon: Zoom387 at 6th of 133

2026 season ranking, Cameroon: 6th of 133

Zindi all-time global ranking: Zoom387 at 462nd of 17,834

All-time ranking, global: 462nd of 17,834

Zindi 2026 season global ranking: Zoom387 at 422nd of 5,085

2026 season ranking, global: 422nd of 5,085

#Zindi#Machine Learning#Time Series#Remote Sensing#LightGBM#CatBoost#Agriculture

Share

XLinkedInWhatsApp

Comments

Comments use GitHub Discussions. Sign in with your GitHub account.