An empty shelf makes demand look low.
When a clinic is out of ACTs for 20 days, the report shows 20 days of dispensing. Plan from that and the next delivery is short too. StockSense corrects for the days out of stock and flags it.
StockSense reads monthly LMIS reports, predicts which primary health care facilities will run out of ACTs, ORS + zinc or oxytocin, works out how much to send, and drafts the resupply memo. The model does the counting. The AI only explains, and every number it writes is checked against the data.
Tracks the medicines that cannot run out
A stockout is usually visible weeks before it happens. The shelf is getting thin, the rains are coming, the last delivery was short, and the facility stopped reporting. But those signals sit in different spreadsheets, and the monthly report only says what was dispensed, not what patients needed.
When a clinic is out of ACTs for 20 days, the report shows 20 days of dispensing. Plan from that and the next delivery is short too. StockSense corrects for the days out of stock and flags it.
ACT and RDT use climbs with the rainy season. A fixed months-of-stock rule notices after shelves are bare. The model learns each facility's seasonal pattern and plans life-saving items to the high forecast.
Facilities that skip reports are often the ones in trouble. Reporting rate is a model feature and a reason code.
Balance errors, impossible stockout days and outliers are flagged before a single forecast is made.
Without a ranked plan, scarce stock goes to whoever asked first. StockSense ranks by risk × how critical the item is.
One scoring job, cron-ready, turns last month's reports into a ranked resupply plan and a memo a logistics officer can sign.
DHIS2 or LMIS exports are validated, mapped to the facility registry and checked: balance equation, gaps, outliers. Stockout months are corrected for hidden demand.
LightGBM forecasts monthly use as a range (p10, p50, p90) and a calibrated classifier gives the chance of a stockout in 4 and 8 weeks.
A min-max policy works out how much to send, ranked by risk × criticality, within the budget or truck space you set. Each line says why.
The AI writes the memo from a fact pack of pre-computed numbers. A number guard rejects anything it can't trace to a row.
These are the numbers the scoring job produced: 8-week stockout risk for every facility and medicine, and the resupply plan. Hover a facility to see its riskiest item.
| Facility | Risk | Weeks left | Send |
|---|
This runs the trained model itself, not an animation of it. Pick a facility, move a slider, and the real LightGBM models re-score it: calibrated risk, forecast range, the optimiser's order, every input's contribution, and the path through an actual decision tree. Nothing you do here is saved.
The tick under each slider marks what the facility actually reported. Inputs you don't touch keep their real values, as do the other 25 inputs the model reads.
SHAP values for the 8-week classifier. Red pushes the risk up, green pulls it down. They add up exactly to the model's score.
History, the forecast range and the three strongest reasons, the same view a PHC coordinator gets in the dashboard.
What pushed this prediction up or down the most.
Months out of stock all month are left unknown, not guessed.
Language models are good at writing and bad at arithmetic. So StockSense never lets one compute a quantity. It writes from a fact pack where every number is already formatted by code, cites the facts it uses, and a guard rejects the draft if any number can't be traced.
Risks, weeks of stock and quantities come from the model and optimiser, formatted once, with the source row recorded.
OpenAI Structured Outputs write the headline, risks, actions and caveats, citing fact IDs. No quantity is calculated in a prompt.
One retry with the guard's feedback, then a plain template. Across 40 test memos, 100% passed.
Type one facility's numbers. The calculator uses the same correction and min-max rule as the optimiser. Where the app uses the model's forecast for monthly need, here you type your own estimate. It runs in your browser; nothing is sent.
Rolling-origin backtest: at each of 12 past months the model was trained only on data up to that month, then scored on what happened next. Compared against the two rules a logistics team would use anyway.
1.0 means as good as last month's value. Below 1.0 beats it.
AUROC: ranking quality (higher is better). Brier: probability error (lower is better). Recall @ top 10%: share of real stockouts caught if you only check the riskiest tenth.
Calibration: when it says 30%, about 30% should run out. Closer to the diagonal is better.
Same history, three ways of deciding what to send.
Read these honestly. All results are on synthetic data from the built-in simulator, which encodes our own assumptions about seasonality, shocks and reporting. A model that does well here may do worse on real LMIS data. Re-run make eval on your own data and read the model card before relying on it.
“Which facilities will run out of ACTs or ORS + zinc next month, and how much should I send?”
“Why is my facility flagged?”
Clone it and start the stack: Postgres, the API, the dashboard and this site. The first start generates synthetic data, runs the backtest and trains the model. Then load your DHIS2 export and retrain.
docker compose up --buildSimulated data is marked in the database, the dashboard, the memo and this page. It is never shown as real.
Each memo stores its fact pack, and each fact records the predictions or plan row it came from.
The plan and memo are drafts for a logistics officer to check. Data-quality caveats are part of every memo.
Self-hosted. Stock reports hold no patient data. The LLM is optional and only sees the fact pack.
Backtest metrics against simple baselines ship with every model run, with a calibration plot on the model card.
Monthly data only, no in-transit stock in standard exports, and a model that must be retrained as supply practice changes.
No. Quantities come from a min-max inventory rule applied to the model's forecast, and optionally a linear programme when budget or truck space is limited. The AI only writes the explanation, and the number guard rejects any number it did not copy from the fact pack.
Not in the demo. The demo runs on a simulator that invents 300 facilities across Enugu State's 17 LGAs, with IDs starting SYN-. Every synthetic row is labelled, and the site and dashboard show a SYNTHETIC banner whenever it is in use.
Load the facility registry, then monthly reports with facility, period, commodity, opening balance, received, consumed, losses, closing balance and days out of stock. Common DHIS2 headers are recognised and commodity names are mapped through aliases in a config file. Then train and score. Start from an empty database so synthetic and real data never mix.
Reports are monthly, so the 4-week horizon is next month and the 8-week horizon is the month after. Deliveries within a month are not modelled.
No. Without a key, a deterministic template writes the memo from the same fact pack, and the same guard checks it. With a key, the model set in OPENAI_MODEL writes it using Structured Outputs.
ACTs, malaria RDTs, ORS + zinc, amoxicillin DT, oxytocin, family planning implants and iron-folate. The list, units, min and max months and lead times live in one config file you can edit.
Nothing. It is MIT-licensed open source. You pay only for your own hosting and, if you choose, OpenAI API usage.