All work

FFB Yield Forecasting

Forecasting palm oil yield 18 months ahead for 121 estates across 6 regions, with an honest uncertainty band, and numbers that add up from estate to region to country.

Direct multi-horizon forecastingGradient boostingBottom-up reconciliationPython

To protect enterprise data, only high-level techniques from the real project are shared. The worked example uses a public dataset (London smart-meter energy use) with the same shape of problem, so every number there can be reproduced.

121

palm oil estates across 6 regions

18 months

forecast horizon

3 levels

estate → region → country

7%

MAPE at country level

The problem

The company runs 121 palm oil estates across 6 regions. Budgets, labour, logistics and sales commitments are all planned well before the crop comes in, so planners need to know roughly how much fresh fruit bunch (FFB) each estate will produce, long before harvest.

121 PALM OIL ESTATES ACROSS MALAYSIA

Each dot represents one estate · illustrative placement

121 estates6 regions
PENINSULARSARAWAKSABAHSouth China Sea
Schematic footprint of 121 estates. Dots do not indicate actual estate locations or regional allocation.

Three things were being asked for at once:

  • See far ahead. A forecast for each of the next 18 months, not just next month.
  • Show the uncertainty. A single number hides risk. Planners need a likely range.
  • Make the numbers add up. If estate forecasts do not sum to the regional figure, nobody trusts any of them.
PALM HARVESTING / FIELD CYCLE00 / 06
FIELD ASSESSMENTMature oil palmLoose fruit checkUnripeRipeRipeness gaugeCUTTING & COLLECTIONHarvesterChisel / EgrekInter-row frond pileLoose fruit pick-upBuffalo cartHAUL TO MILLCollection Point (TPH)FFB LorryMillMill ramp
Fronds / palmRipe FFBLoose fruits (brondolan)Truck / transport route
Round every 7–10 days · FFB 15–25 kg · Mill within 24 h

0. Full-colour overview

The complete field cycle: palm and fronds in purple, ripe bunches in red, loose fruits in pink, and transport in blue.

Why it is harder than it looks

Think of it like forecasting exam results for 121 schools, 18 months early, and the district total must equal the sum of the schools.

RequirementWhat usually goes wrongWhat this project does
Long horizon (18 months)A one-step model is fed its own predictions again and again, so small errors snowball.Train a separate model for each month ahead (direct forecasting).
UncertaintyA single line looks precise but says nothing about risk.Report a p10 to p90 band around the central forecast.
121 estates, 3 levelsSeparate models per level disagree with each other.Forecast at the bottom, then sum upwards (bottom-up reconciliation).

The approach

Step 1: Describe each month with three kinds of signal

Feature groupWhat it capturesExamples
Time-dependent (T)Where the series is headingTrend, recent past values (lags)
Seasonal (S)Patterns that repeat on a calendarFourier terms (sine and cosine waves)
Exogenous (X)Outside drivers of the cropRainfall

Step 2: One model per horizon (direct forecasting)

Instead of one model that predicts next month and then feeds on its own output, a separate model is trained for each step ahead. No prediction is ever used as an input, so errors cannot compound.

T−n…T−4T−3T−2T−1Model f1T+1T−n…T−4T−3T−2T−1Model f2T+2T−n…T−4T−3T−2T−1Model f3T+3
Observed valuePredicted valuePredictors (lags)
Direct forecasting: the same inputs, a different model for each step ahead. Adapted from skforecast.
To predict 1 month ahead (t+1), we use data up to time t:ŷt+1 = f1(Xt, Tt, St)To predict 2 months ahead (t+2), we also use data up to time t:ŷt+2 = f2(Xt, Tt, St)⋮In general, for h months ahead:ŷt+h = fh(Xt, Tt, St)

y is FFB yield, h is months ahead (1 to 18), f is a gradient-boosted tree model trained for that horizon.

Step 3: Put a range around every forecast

Each forecast is published as a central line (p50) with a p10 to p90 band: "we expect the result to land inside this band about 8 times out of 10."

Ten plausible futures → p10–p90 bandRedrawn from the supplied Australia arrivals illustration, not measured project data. Observed history draws first, then ten plausible futures fade into a p10–p90 band with a central p50 line. The band is illustrative, not a calculated interval from ten samples.Ten plausible futures → band024681019801990200020102020YearVisitors (millions)Observedp50p10–p90

10 futures → 1 band

Many plausible futures are summarised into a shaded band, which is what planners see on the dashboard. Redrawn from the supplied Australia arrivals illustration.

LightGBM quantile regression learns a separate lower and upper forecast for each horizon, using the same data available up to time t. Set the quantile objective’s alpha to 0.10 for p10 and 0.90 for p90.

P10 — lower forecast (α = 0.10)

ŷt+h, 0.10 = fh, 0.10(Xt, Tt, St)

P90 — upper forecast (α = 0.90)

ŷt+h, 0.90 = fh, 0.90(Xt, Tt, St)

Each model minimises the sum of the quantile (pinball) loss over its training examples. With residual u = actual − predicted, the two losses are:

ρ0.10(u) = max(0.10u, −0.90u)

ρ0.90(u) = max(0.90u, −0.10u)

The p10 loss penalises overprediction more strongly; the p90 loss penalises underprediction more strongly. Together, the forecasts define a nominal 80% prediction interval — actual coverage should be checked on held-out data.

Pinball loss / How α works

Illustrative seeded data · one straight-line model

Same data, different line

-202xy
OLS (squared)P50 (pinball)

Cost of getting it wrong

overprediction (u < 0)underprediction (u > 0)residual u = actual − predicted
ρα(u) = max(αu, (α − 1)u)

Step 4: Add it up from the bottom

Forecasts are made only at estate level. Regional and country figures are the sum of the estates beneath them, so every level agrees by construction.

Yield(region) = Σ Yield(estates in that region)

Yield(country) = Σ Yield(regions)

Worked example on open data

Because the estate data is confidential, the same recipe is demonstrated on daily smart-meter readings from London homes, joined with London weather. It has the same three ingredients: a hierarchy, strong seasonality and a weather driver.

Real projectOpen-data proxy
TargetEstate FFB yieldHousehold energy use (kWh per day)
HierarchyEstate → Region → CountryHousehold → Borough → London
Outside driverRainfallTemperature, cloud cover, solar radiation
SeasonalityYearly palm oil crop cycleWeekly cycle (Fourier terms, period 7)
Horizon18 months ahead7 days ahead
ModelsOne per horizonOne per borough per horizon: 12 × 7 = 84

Daily energy consumption in London

Average household energy use
Daily energy consumption in LondonTraced from the supplied November 2011 to February 2014 chart. Daily household energy use peaks in winter and falls in summer, ranging from roughly 7 to 16 kWh. This drawing is not the original dataset.810121416Nov 2011Nov 2012Nov 2013Energy (kWh/day)Date
Average daily household energy use across London, Nov 2011 to Feb 2014 (827 days, about 3.5 million rows). Redrawn from the supplied chart.

Which weather signals move energy demand?

London: 7-day forecast vs actual

London: forecast with P10–P90 prediction interval

London: forecast with P10–P90 prediction intervalActual and forecast mean household energy use from 21 to 27 February 2014, with a shaded band showing mean household P10 and P90 quantiles. Interval boundaries are visually traced from the supplied chart, not original measurements or a calibrated London-level interval.810121421222324252627February 2014Mean kWh per household
ActualForecastP10–P90
Mean of household quantiles. Band boundaries are redrawn from the supplied chart; averaging household quantiles does not guarantee a calibrated 80% interval for London.

Forecast error at each level of the hierarchy

Noise cancels as forecasts roll up. Error drops about 5× from household to London.

Household-level error by forecast horizon

Error on day +7 is close to day +1. Direct forecasting keeps far horizons from drifting.
  • Features (17): last 7 days of usage, today's usage, weekly Fourier terms, calendar fields, mean temperature, cloud cover, solar radiation, household ID.
  • Model: XGBoost (300 trees, depth 6, learning rate 0.03), one per borough per horizon.
  • Test: the final 7 days of every household were hidden; forecasts made from 20 Feb 2014.
  • Reconciliation: household forecasts averaged up to borough, then London.
LevelSeriesMAE (kWh/day)MAPE
Household4,9841.94n/a
Borough120.393.7%
London10.373.6%

Reality check

  • One test window. Scored on a single 7-day window; a rolling backtest would be sturdier.
  • A consistent lean. The London forecast was above actual on 6 of 7 days, a small bias worth correcting.
  • No baseline yet. Not yet compared against a simple "same day last week" forecast.
  • Boroughs are stand-ins. Households were split into 12 equal groups to create the middle level.
  • Not every home could be forecast. 582 households had no reading on the start date.

Key takeaways

01

For long horizons, do not let a model eat its own predictions.

02

Forecast at the bottom and sum upwards, so every level agrees automatically.

03

Always publish the range. A band turns a forecast from a promise into a planning tool.

04

Judge the forecast at the level where the decision is made.

Tech stack

PythonpandasNumPyXGBoostLightGBMstatsmodelsFourier featuresPlotlyBottom-up reconciliation