Isan rice yield benchmark: forecasting models
Trained on synthetic data. These models were fitted to a simulated Northeast Thailand whose data-generating process is documented and whose true yield components are known. They show how the paper's pipeline behaves; they are not forecasts of Thai rice yields and must not be used as such. To forecast real yields, rebuild the features from real inputs and retrain with the code (
make all).
Main-season rice yield (kg/rai) of a district, forecast at the end of the season (November information set) from 81 leakage-safe features in eight groups: previous yields, humidity and soil moisture, water (rain, reservoirs, irrigation), climate, satellite indices (NDVI, EVI, LSWI, LST), prices of rice and competing crops, geography, and map layers with spatial lags. Companion models to the preprint Leakage-safe, explainable multi-source machine learning for district-level rice yield forecasting: a ground-truth simulation benchmark for Northeast Thailand by Thanatchon Chongmankhong (read online · code · data · DOI 10.5281/zenodo.24036674).
Independent personal research. Does not represent the views of any employer or institution.
The model
The paper's main model, selected on the validation seasons 2015-2017: the equal-weight average of
- LightGBM (
lightgbm.txt, native format), with the hyperparameters tuned on 2015-2017:num_leaves=63,learning_rate=0.05,n_estimators=400,min_child_samples=80,subsample=0.8,colsample_bytree=0.5,reg_lambda=1.0,max_depth=-1; - Ridge regression (
ridge.json): median imputation, standardisation and a ridge fit whose penalty RidgeCV chose (alpha = 0.53).
Both are refitted here on all 3,000 district-seasons with features in the main world (200 districts, seasons 2011-2025; 2008-2010 serve only as yield history), the information a forecaster would have before the 2026 season. The level of yield is the target.
Prediction intervals. conformal.json holds split-conformal offsets: empirical quantiles of the ensemble's
out-of-sample errors on every season it could forecast with at least three seasons of training data
(2014-2025, 2,400 forecasts). Added
to a point forecast they give 80% and 90% intervals. In the paper's rolling evaluation the 80% intervals covered
78.3% of outcomes; the worst season (2018) covered 40%.
Accuracy (from the paper, synthetic benchmark)
Rolling origin: each test season 2018-2025 is forecast by models trained on earlier seasons only (1,600 forecasts). RMSE and MAE in kg/rai (multiply by 0.00625 for t/ha); brackets: 95% season-block bootstrap interval; Skill = 1 - MSE / MSE of climatology.
| Model | RMSE [95% CI] | MAE | MAPE (%) | R2 | Skill |
|---|---|---|---|---|---|
| LightGBM + Ridge ensemble (this model) | 29.9 [24.7, 37.4] | 23.4 | 7.0 | 0.87 | +0.60 |
LightGBM alone (lightgbm.txt) |
27.4 [26.1, 29.0] | 21.7 | 6.4 | 0.89 | +0.66 |
Ridge alone (ridge.json) |
37.9 [25.4, 54.3] | 27.9 | 8.3 | 0.78 | +0.35 |
| Climatology (previous 3-season mean) | 47.1 [40.6, 53.2] | 37.8 | 11.4 | 0.67 | +0.00 |
LightGBM alone had the lowest test error, but the ensemble was the model chosen on the validation seasons, before
the test seasons were seen; the paper reports both. Holm-adjusted Diebold-Mariano tests over eight test seasons
were not significant. Ridge failed badly in some seasons, which a post-hoc analysis traced to its row-wise
leave-one-out penalty choice (Section 5.8 of the paper); predict.py returns each component so you can use
LightGBM alone.
Files
| file | content |
|---|---|
lightgbm.txt |
LightGBM booster (native text format) |
ridge.json |
Ridge pipeline as numbers: imputation medians, scaler means and scales, penalty, coefficients, intercept |
features.json |
the 81 input columns in order, their groups, target and units |
conformal.json |
80% and 90% split-conformal offsets (kg/rai) and the calibration seasons |
metrics.json |
the rolling-origin test metrics above, for every model in the paper |
predict.py |
loader: load(folder).predict(X) -> LightGBM, Ridge, ensemble and interval columns |
sample_input.csv, sample_output.csv |
the 200 districts of season 2025 and their predictions (a check of the export, not a forecast) |
Usage
from huggingface_hub import snapshot_download
import sys, pandas as pd
folder = snapshot_download("thanatchon36/isan-rice-yield-benchmark")
sys.path.insert(0, folder)
from predict import load
model = load(folder)
X = pd.read_csv(f"{folder}/sample_input.csv")
print(model.predict(X)[["ensemble", "lo80", "hi80"]].head())
To build the features for new seasons, use the code repository: put the raw tables in data/raw/ (schema in
docs/DATA_SCHEMA.md), then
from isanrice.features import build_features, load_raw
df, groups = build_features(load_raw()) # one row per district-season
X = df[df.year == 2026] # columns as in features.json
Features are built so that a season's row uses only information available by the end of that season; the repository's tests check this.
Limitations
- Synthetic training data: the simulator decides which mechanisms exist and how strong they are. The model has learned that world, not Thailand.
- One region, one crop (main-season rice), district level, end-of-season information set. Earlier cut-offs (July-October) need models trained on the matching features (E5 in the paper).
- The explanations of these models are only partly faithful: grouped SHAP recovered drought (r = 0.84) but misattributed heat stress to moisture and absorbed the technology trend into prices.
Reproduce
make setup && make all in the code repository runs the experiments; make model retrains these files
(isanrice 0.1.0, LightGBM 4.7.0, scikit-learn 1.9.1).
Citation
@misc{chongmankhong2026isanrice,
author = {Chongmankhong, Thanatchon},
title = {Leakage-safe, explainable multi-source machine learning for district-level rice yield forecasting:
a ground-truth simulation benchmark for Northeast Thailand},
year = {2026},
doi = {10.5281/zenodo.24036674},
url = {https://thanatchon36.github.io/isan-rice-yield-benchmark/paper/},
note = {Preprint; all results are from a synthetic benchmark}
}
Dataset used to train thanatchon36/isan-rice-yield-benchmark
Evaluation results
- RMSE (kg/rai) on Isan rice yield benchmark (synthetic)self-reported29.900
- MAE (kg/rai) on Isan rice yield benchmark (synthetic)self-reported23.400
- R2 on Isan rice yield benchmark (synthetic)self-reported0.870