Instructions to use unnicr/dead-stock-predictor with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Scikit-learn
How to use unnicr/dead-stock-predictor with Scikit-learn:
from huggingface_hub import hf_hub_download import joblib model = joblib.load( hf_hub_download("unnicr/dead-stock-predictor", "sklearn_model.joblib") ) # only load pickle files from sources you trust # read more about it here https://skops.readthedocs.io/en/stable/persistence.html - Notebooks
- Google Colab
- Kaggle
Dead-Stock Predictor
Predicts which items will sell nothing in the next 90 days (13 weeks), often while they are still selling a little. Add stock on hand and cost, and it ranks the money you are about to get stuck with and tells you what to do.
It needs only what every ERP already has: sales lines (date, product, quantity) and optionally stock (product, on hand, unit cost).
Try it: the web app runs entirely in your browser, so your sales data is never uploaded. Click Try sample data to see a 180-item example.
Why not just "no sale for 90 days"?
That rule only fires after the money is already stuck. This model looks at the shape of each item's demand:
- how long since the last sale, compared with the item's own rhythm between sales (a slow mover that usually sells every 6 weeks isn't dead after 4),
- whether sales are fading (last 13 weeks vs the 13 before, last 4 vs last 26),
- volatility and how many weeks it sold at all,
- the same season last year and two years ago, so a Diwali gift pack or an umbrella in the off-season isn't written off when its season is coming back.
Results
These are on 1,500 items never seen in training, evaluated at 7 different dates and on a shifted calendar.
| Precision | Recall | |
|---|---|---|
| Model (risk β₯ 50%) | 78.9% | 82.0% |
| Rule: no sale for 13+ weeks | 78.3% | 61.7% |
| Rule: no sale for 8+ weeks | 74.9% | 74.0% |
| Model at the same recall as the 8-week rule | 81.6% | 74.0% |
- Ranking quality (AUC): 0.954, against 0.924 for "weeks since last sale" alone.
- Items still selling in the last 8 weeks: rules can't flag any of these, but the model catches 48% of the ones that are about to die, at 72% precision.
- False alarms are mostly near-dead: 72% of them sold only 1β2 units in the next 13 weeks.
- Calibrated: items given 70β90% risk were dead 82% of the time, and items under 10% risk were dead 2% of the time.
- By item type (dead items caught): fast movers 95%, launches and fads 93%, discontinued lines 92%, slow movers 76%, fading products 74%, seasonal items 68%. Seasonal items also get the most false alarms (16%), because a single stray off-season sale counts as "alive".
On the included sample export (180 items), the model flagged items holding 89% of the stock value that really went dead.
Full log: train_log.txt.
Honest limits. The model is trained on realistic simulated sales histories: fast and slow movers, festival, monsoon and school seasonal items, fading products, launches and fads, discontinued lines, returns and stray sales. Real data will differ. Before trusting it for decisions, check the predictions against 3 months of your own history. It also can't know about things outside the sales data, like a planned promotion, a supplier stock-out or a price change.
What it tells you
| Risk | Stock | Action |
|---|---|---|
| β₯ 70% | yes | Clear now: discount, bundle or return to supplier |
| β₯ 70% | none | Likely dead: stop reordering |
| 40β70% | β | Stop reordering and watch |
| < 40% | more than 26 weeks of cover | Overstocked: reduce the next order |
| < 40% | normal | Healthy |
Value at risk = stock value Γ risk, and the results are sorted by it.
Use it
Web app: upload the sales CSV, and optionally the stock CSV. Columns are detected automatically and you can change them. Then download the report.
Command line (plain Python 3.8+, nothing to install):
python predict_csv.py sales.csv --stock stock.csv
python predict_csv.py sales.csv --stock stock.csv --as-of 2026-09-30 --out dead_stock_report.csv
Dates may be 2026-09-27, 27-09-2026, 27/09/2026 or 27-Sep-2026 (day first). Returns may be negative quantities.
Exporting from ADempiere / iDempiere (PostgreSQL):
-- sales lines, last 2 years (credit memos as negative)
SELECT i.DateInvoiced, p.Name AS ProductName,
CASE WHEN dt.DocBaseType = 'ARC' THEN -il.QtyInvoiced ELSE il.QtyInvoiced END AS QtyInvoiced
FROM C_InvoiceLine il
JOIN C_Invoice i ON i.C_Invoice_ID = il.C_Invoice_ID
JOIN C_DocType dt ON dt.C_DocType_ID = i.C_DocType_ID
JOIN M_Product p ON p.M_Product_ID = il.M_Product_ID
WHERE i.IsSOTrx = 'Y' AND i.DocStatus IN ('CO','CL') AND i.AD_Client_ID = <client>
AND i.DateInvoiced >= now() - interval '2 years';
-- stock on hand with current cost (iDempiere: M_StorageOnHand; ADempiere 3.x: M_Storage)
SELECT p.Name AS ProductName, SUM(s.QtyOnHand) AS QtyOnHand, MAX(c.CurrentCostPrice) AS CurrentCostPrice
FROM M_StorageOnHand s
JOIN M_Product p ON p.M_Product_ID = s.M_Product_ID
LEFT JOIN M_Cost c ON c.M_Product_ID = p.M_Product_ID AND c.AD_Client_ID = p.AD_Client_ID
WHERE p.AD_Client_ID = <client> AND p.IsActive = 'Y'
GROUP BY p.Name;
If you have several cost elements or accounting schemas, filter M_Cost to the one you use. For POS sales, use C_OrderLine with C_Order.DateOrdered instead of invoices.
Python:
from deadstock import DeadStockModel, analyze, parse_date
model = DeadStockModel.load("model.json")
sales = [("Sunrise Tea 250g", parse_date("2026-09-20"), 4), ...] # (product, date, qty)
stock = {"Sunrise Tea 250g": (40, 118.5)} # product: (on hand, unit cost)
as_of, rows = analyze(sales, stock, model=model)
for r in rows[:10]:
print(r["product"], round(r["risk"] * 100), "%", r["action"])
How it works
- Sales lines are bucketed into 104 weekly totals per product, counting back from the analysis date (week 0 = the last 7 days).
- 18 demand signals are computed per product: recency, sales in the last 4, 13, 26 and 52 weeks, active weeks, trend, the mean gap between sales and how overdue the next sale is, age, the same 13 weeks last year and two years ago, a Β±6-week window around last year's season, volatility and rate.
- A scikit-learn
GradientBoostingClassifier(300 trees, depth 4) gives the probability of zero sales in the next 13 weeks. It is exported tomodel.json. - Stock and cost turn the probability into value at risk, weeks of cover and an action.
deadstock.py (Python) and space/deadstock.js (browser) are the same algorithm. test_parity.py checks that they agree.
Retrain
pip install -r requirements.txt
python train.py # simulates 7,500 items x 3 years, trains, evaluates, writes model.json
python make_sample.py # remakes the demo export in data/
python test_parity.py # browser version == Python version (needs Node.js)
The best improvement is to train on your own history: take snapshots at past dates, label each item by whether it
sold in the following 13 weeks, and feed those samples to train.py in place of the simulated ones.
Files
| File | Purpose |
|---|---|
model.json / model.joblib |
Trained model (JSON for Python and the browser, joblib for scikit-learn) |
deadstock.py |
Date parsing, weekly history, features, scoring, actions |
predict_csv.py |
Command-line tool for ERP exports |
simulate.py, train.py |
Sales simulator and training/evaluation |
make_sample.py, data/ |
Demo export (sales, stock, and what really happened next) |
space/ |
The web app (static Hugging Face Space) |
- Downloads last month
- -