Framing a problem as a model
Leadership asks for "AI to fix out-of-stocks". Before any model exists, someone has to say which decision it will change, what exactly it predicts, how it will be judged and what it has to beat. This lesson turns that request into a one-page framing you can defend, and shows you how to tell when a better rule is the honest answer.
By the end you can
- Turn a vague request into a decision, a target with its unit and horizon, a metric pair and a baseline
- Tell regression, classification, ranking and anomaly detection apart, and pick the one the decision needs
- Spot labels that lie, such as sales on days the shelf was empty, and choose how to handle them
- Compute a baseline forecast and its error in pandas, and write a one-page framing around it
- Size the value roughly and decide whether machine learning is worth building at all
The everyday picture
A weather forecast on its own changes nothing. It becomes useful when someone acts on it: you take an umbrella, a farmer brings the harvest in a day early, a fishing crew stays in port. Each of them needs a different prediction. You need the chance of rain this afternoon where you live. The farmer needs the total rain over the next three days. The crew needs the wind at sea tomorrow at dawn. Same sky, three predictions, because there are three decisions.
Forecasters also know a forecast has to earn its place. The simplest forecast there is says "tomorrow will be like today", and weather services measure their skill against simple rules like that one. A forecast that can't beat "like yesterday" isn't worth broadcasting. Two more details carry over to your work. The forecast can only use readings taken before it was made. And the cost of being wrong is lopsided: carrying an umbrella you didn't need costs little, getting soaked on the way to an interview costs a lot, so you carry one at a 30% chance of rain.
Machine learning at work has the same shape. Start from the decision, predict what that decision needs using only what is known at the time, weigh the two kinds of mistake, and prove you beat the simple rule.
Start from the decision
"Use AI for out-of-stocks" describes a worry. A model can't act on a worry. Your first job is to find the decision a prediction could improve, because a prediction is only ever an input to a decision. If no decision changes, the model's accuracy doesn't matter to anyone.
At Mangrove Market, out-of-stocks touch several decisions, each with a different owner and rhythm:
- The nightly replenishment order from each of about 320 stores to its distribution centre (DC). Supply chain owns it, a system computes it at 22:00, and it covers every product each store carries: millions of order lines a night.
- The DCs' weekly purchase orders to suppliers, owned by the buyers.
- Which substitute the app offers when an item is cut from an order, owned by the app team.
- How much shelf space each product gets, revised a few times a year.
Pick one. They need different predictions (daily demand per store, weekly demand per DC, an order of substitutes, long-run sales rates), and a project that tries to serve all four serves none. Tomás, the data analyst who is moving into data science, picks the nightly store order for three reasons. It runs every night, so gains add up fast. Its formula already exists, so a better forecast can drop in without changing anyone's job. And the stock-out it causes is the one customers see: about 9% of app orders have an item cut today.
Here is today's rule, written down with one store's numbers:
order tonight = 4 days x average daily sales of the last 28 days - stock on hand
Store 117, milk 1 litre
average daily sales, last 28 days 30 units
cover for 4 days 4 x 30 = 120 units
stock on hand at close 45 units
order tonight 120 - 45 = 75 unitsLook closely and you can see a forecast hidden inside the rule. "Average daily sales of the last 28 days" is a prediction of each of the next four days' demand. That is the part a model can replace. The rest (four days of cover, minus stock on hand) is a policy that supply chain owns. Keep that split clear. If you change the forecast and the policy at once and stock-outs fall, nobody can tell which change worked.
Before going further, write down five answers. Who acts on the prediction? How often? What exactly can they change? What does a wrong prediction cost in each direction? How long is it between deciding and seeing the result? For the store order: the replenishment system, every night, the order quantity, waste and crowded back rooms when it is too high or empty shelves and cut app orders when it is too low, and about a day of lead time.
Quick check
Which part of today's ordering rule should a model replace first?
The 28-day average is the forecast inside the rule, so it is what a model replaces. Days of cover is a policy supply chain owns, and changing it at the same time would hide which change helped. Stock on hand is a measurement; it can be wrong (stock accuracy is about 82%), and fixing it is a data-quality job.
Target, unit and horizon
Once the decision is fixed, three choices define the prediction. The unit is one row: here one store, one product, one day, because that is the grain at which the order is placed. The target is the number the model outputs for that row: units customers wanted to buy. The horizon is how far ahead you predict. The order goes at 22:00, the truck arrives the next morning, and the order has to cover tomorrow to day 4, so the forecast must cover days 1 to 4 after the order night.
The horizon fixes what you may use. At 22:00 you know sales up to today's close, tonight's stock on hand, promotions planned for next week and the festival calendar. You don't know tomorrow's sales, tomorrow's weather or tomorrow's closing stock. A forecast for day 4 rests on older information than one for day 1, so it is usually worse; in Forecasting demand you will report error per horizon.
This is supervised learning: you collect past examples where you know both the inputs and the outcome, and the model learns the link between them. Each training row is one store-product-day from history. The features are what was known at 22:00 the night before: sales over the last 28 days, the same weekday last week, day of the week, whether a promotion was planned, days to the next festival. The label is what happened: units sold that day. In Mangrove's tables a store or DC is a site and a product is an article, so the history lives in daily_sales(store_id, article_id, sale_date, units_sold).
The shape of the target decides the kind of task. Most business machine learning falls into four kinds:
| Task | It outputs | Mangrove example | In this course |
|---|---|---|---|
| Regression | A number | Units of milk store 117 sells tomorrow | ds-03, ds-04 |
| Classification | A probability for a category | Will this app customer stop ordering next month? | ds-05 |
| Ranking | An order for a list | Which substitutes to show first when an item is cut | not covered |
| Anomaly detection | A flag on what looks unusual | A partner order with an extra zero | ds-06 |
Forecasting is regression where the rows are ordered in time. The past predicts the future, and the future must never leak back into the features; Features and leakage is about exactly that.
The same worry can be framed several ways. "Will milk run out at store 117 tomorrow?" is classification. It sounds closer to the problem, but its answer depends on tonight's order, which is the thing you are trying to set, and a yes or no can't tell the formula how many units to order. Demand per day is regression, and it plugs straight into the order. Frame the prediction as the input the decision needs.
Quick check
A colleague proposes a model that predicts "will this product be out of stock at this store tomorrow, yes or no" to improve the nightly order. What is the main problem?
The decision is how many units to order. A yes or no can't size the order, and whether an item runs out depends on the order you haven't placed yet. Class imbalance is a real issue for classifiers (ds-05), but it isn't the reason this framing fails.
Labels that lie: censored demand
The label is supposed to be what customers wanted. What the data records is what they bought. On most days the two match. On a day the shelf goes empty at 3 pm, they don't: the till records 12 units sold, but customers who came after 3 pm wanted more and left without it. That is censored demand: the true number is at least what you saw, and you don't know by how much. For Mangrove's fast movers, about 1 in 9 store-product-days has some out-of-stock time (approximate).
Censored labels do quiet damage. A model trained on raw sales learns that demand is lower than it really is, most of all for the products that run out most often. A low forecast produces a small order, the small order empties the shelf again, and the empty shelf produces another low sale to learn from. That is the dashed arrow in the first diagram. Today's 28-day average has the same flaw, and it is part of why it under-forecasts by about 9%.
You have three options, from cheap to careful:
- Drop censored days from training and from scoring. It is easy and it removes the worst lies. The catch: stock-outs cluster on the busiest days (festival eves, promotions), so dropping them can remove exactly the peaks the model most needs to learn.
- Keep them with a lower weight in training, and leave them out when you score the model. The model still sees the busy days, but trusts them less. Don't add "was out of stock that day" as a feature for the day you predict: at 22:00 the night before, nobody knows it yet.
- Estimate the lost demand. The app gives a direct signal: when an item is cut from an app order, a customer asked for it and didn't get it, so cut app lines are a floor for lost demand. For store sales, scale up from the sales before the shelf emptied. If 12 units sold by 3 pm on a day that usually sells 60% of its units by 3 pm, the full-day demand was about 12 ÷ 0.6 = 20.
How do you know a day was censored? The cleanest signal is on_hand_close in stock_snapshot. If the store closed with stock left, the shelf probably never emptied. It is a proxy, and an imperfect one: with stock accuracy around 82%, some "in stock" days had stock the system believed in that wasn't on the shelf. Write down the rule you used and how many rows it removed, because every later number depends on it. You will build this flag in SQL in the practice task.
Quick check
A store sold 12 loaves of bread by 2 pm, then the shelf was empty for the rest of the day. On normal days it sells about 40% of its bread by 2 pm. What is a fair estimate of that day's demand?
If 40% of the day's demand usually arrives by 2 pm, then 12 ÷ 0.4 = 30. Using 12 as the label is the censoring trap: it teaches the model that the busy day was a quiet one. 17 adds 40% to 12 instead of dividing by 0.4.
Two metrics: one for the model, one for the business
A model metric tells you how close the forecast is. A business metric tells you whether anything got better. You need both, and you need to agree them before you see any results, so nobody picks the metric that flatters the model afterwards.
For demand forecasts the main model metric in this course is WAPE. It adds up every miss, ignoring direction, and divides by the total actual units, so a product that sells 300 units a day counts more than one that sells 2. Pair it with bias, which keeps the sign and tells you whether you are too high or too low overall. A forecast can have a decent WAPE and still under-order every single day.
day actual forecast |actual - forecast|
Mon 10 12 2
Tue 20 15 5
Wed 30 30 0
sum 60 57 7
WAPE = 7 / 60 = 11.7%
bias = (57 - 60) / 60 = -5.0% (too low overall)Why not the familiar MAPE, the average of each row's percentage error? It divides by each row's actual, so a slow mover with 0 units sold makes it divide by zero, and small sellers dominate it. Forecasting demand compares the error measures properly.
The business metric is what the decision owner cares about. For the store order there are three: the out-of-stock rate (the share of store-product-days with an empty shelf at some point), waste (perishables thrown away) and stock on hand (money tied up and back-room space). They pull against each other. You could drive stock-outs to zero by ordering three times as much, and the waste bill would show why that is a bad idea. So report them as a set and agree limits up front, for example "fewer out-of-stock days, with stock on hand up by no more than about 8% and no rise in milk and bread waste".
The two kinds of metric are measured in different places. Model metrics come from offline evaluation: a backtest on history, where you stand at 22:00 on each past night, forecast with only what was known then, and compare with what happened. Features and leakage shows how to split by time without leaking the future. Business metrics need online evaluation: real orders in real stores. At Mangrove that means a store pilot, where 20 stores order from the new forecast and 20 similar stores keep the old rule for several weeks (Experiments: A/B tests, holdouts and store pilots). Offline gains often shrink online. A better forecast doesn't help if the DC can't pick the extra units, the shelf has no room, or the stock waits in the back room.
The baseline, and whether it is worth building
A baseline is the simplest honest prediction, and every model must beat it on the same rows, the same period and the same metric. Use two:
- Today's rule as a forecast: average daily sales over the last 28 days. The business already runs on it, so it is the bar that matters.
- Seasonal naive: the same weekday last week. It knows nothing but the weekly pattern, which makes it a good test of whether a fancier model learned anything more.
On Tomás's backtest (50 fast-moving products in 20 stores, trained on data up to 30 June 2026 and validated on the eight weeks from 1 July to 25 August), the baselines score like this:
| Forecast | WAPE, all weeks | WAPE, festival week | Bias |
|---|---|---|---|
| Same day last week (seasonal naive) | 39% | 61% | −3% |
| 28-day average (today's rule) | 34% | 58% | −9% (−41% in the festival week) |
The table already shapes the project. The 28-day average beats the seasonal naive overall, because averaging four weeks smooths out noise. Its festival-week bias of −41% shows where the money is: it is slow to notice a jump in demand, so it under-orders exactly when demand peaks. A model has to win there. Later lessons build models to beat these rows, and the best reaches about 24% (Training and comparing models). The rule is plain: a model that doesn't beat the baseline doesn't ship, and a model that beats it by a hair isn't worth a new system to run and watch.
Google's Rules of Machine Learning starts with this rule: "Don't be afraid to launch a product without machine learning." A good rule often gets you most of the value, and it gives you the baseline and the data you will need later. So before building, size the value roughly:
App orders a day 60,000
with an item cut (about 9%) about 5,400
share caused by the top fast movers about 50% (assumption to check)
share a better forecast removes about 1/3 (assumption to check)
orders saved from a cut item about 900 a day
Cost: 2 people for about 6 months at about $5,000 a month each = about $60,000Every number after the first two is an assumption, so label it and check it. Each saved cut keeps a sale and spares a customer the frustration that makes people stop ordering (ds-05). The store shelves add more value on top, since they serve about 400,000 checkout transactions a day. If the sizing comes out small, or the decision can't change (say the DC can't deliver any more often or any more units), stop here. Picking the right problems for AI covers that choice in general.
Last, write down what would make you stop: a backtest that doesn't beat 34% WAPE by at least a few points, or a pilot that cuts stock-outs only by piling up stock. A project without a stop rule keeps going on hope.
Build it
Write the one-page framing for the store order and compute the two baselines' error in pandas, so the page carries real numbers.
Copy the framing template
Fill it in with the decision owner, not alone. Leave the numbers blank until step 3.
Markdown# Framing: <project name> Decision owner: Reviewed with: Date: ## 1. Decision What decision changes, who makes it, how often: What they can change, and what they cannot: Cost of a prediction that is too high / too low: ## 2. Prediction Unit (one row is): Target (the number or class we predict): Horizon (predicted when, for which period): Known at prediction time: Task type (regression / classification / ranking / anomaly): ## 3. Labels Where the label comes from: When the label lies (censored, late, missing) and what we do about it: ## 4. Metrics (agreed before any results) Model metrics: Business metrics and limits: Offline test (backtest period): Online test (pilot design): ## 5. Baselines Today's rule as a prediction: score: Simple second baseline: score: ## 6. Is it worth it? Value if it works (rough, assumptions labelled): Cost to build and run: ## 7. Stop rule We stop, or keep the rule, if:Build both baselines in pandas
The table must have one row per store, product and day, with 0 on days nothing sold; if yours skips zero days, fill them first, or a window of 28 rows won't be 28 days.
shift(1)makes the window end yesterday, so the day being predicted never sits inside its own forecast.Pythonimport pandas as pd # daily_sales: store_id, article_id, sale_date, units_sold (zero days included) sales = pd.read_parquet("daily_sales.parquet") sales["sale_date"] = pd.to_datetime(sales["sale_date"]) sales = sales.sort_values(["store_id", "article_id", "sale_date"]) units = sales.groupby(["store_id", "article_id"])["units_sold"] # Today's rule as a forecast: mean of the 28 days BEFORE the day predicted sales["baseline_28d"] = units.transform( lambda s: s.shift(1).rolling(28, min_periods=28).mean() ) # Seasonal naive: the same weekday last week sales["naive_7d"] = units.shift(7)This is the forecast for the day after the order night. For day 4 the window ends four days earlier, so the same code with
shift(4)gives the day-4 forecast.Flag usable labels and score on the same rows
Join the closing stock, keep in-stock days in the validation weeks, and score both baselines on exactly the same rows.
Pythonstock = pd.read_parquet("stock_snapshot.parquet") stock["sale_date"] = pd.to_datetime(stock["snapshot_date"]) df = sales.merge( stock[["store_id", "article_id", "sale_date", "on_hand_close"]], on=["store_id", "article_id", "sale_date"], how="left", ) df["usable"] = df["on_hand_close"] > 0 # no snapshot: NaN > 0 is False def wape(actual, forecast): return (actual - forecast).abs().sum() / actual.sum() def bias(actual, forecast): return (forecast.sum() - actual.sum()) / actual.sum() in_valid = df["sale_date"].between(pd.Timestamp("2026-07-01"), pd.Timestamp("2026-08-25")) valid = df[in_valid & df["usable"]].dropna(subset=["baseline_28d", "naive_7d"]) for col in ["baseline_28d", "naive_7d"]: print(f"{col:13} WAPE {wape(valid.units_sold, valid[col]):.1%}" f" bias {bias(valid.units_sold, valid[col]):+.1%}")On Tomás's 50 products in 20 stores the output is close to the course's backtest table:
Outputbaseline_28d WAPE 34.2% bias -8.9% naive_7d WAPE 38.8% bias -3.1%Finish the page and review it
Put the baseline scores in section 5, write the value sizing with every assumption labelled, and agree the stop rule with the decision owner. Then ask one question out loud: "If the model hits this number, will you change the order?" If the answer is no, find out why before you write another line of code.
Bring it to your team
Ask these when someone proposes a model, before any work starts.
- Which decision will this change, who owns it and how often is it made?
- What exactly do we predict: one row is what, the target is what, and for which days ahead?
- What do we know at the moment of prediction, and what do we only learn later?
- How do we treat days when the shelf was empty?
- What are the baseline's WAPE and bias on the same backtest, and by how much must we beat them?
- Which business metrics and limits have we agreed, and what result would make us stop?
Red flags
- "Let's see what the data tells us", with no decision named.
- A model accuracy number with no baseline next to it.
- Labels taken straight from sales, with no word about stock-outs.
- Only a model metric, or only a business metric.
- A plan to switch all 320 stores at once, with no pilot.
Practice
Find the usable labels and measure the censoring
Two tables: daily_sales(store_id, article_id, sale_date, units_sold) and stock_snapshot(store_id, article_id, snapshot_date, on_hand_open, on_hand_close). A day is a usable label when the snapshot for that same store, product and date exists and its on_hand_close is above 0. Days with no snapshot are not usable. Return one row per store and product with store_id, article_id, days (all sales days), usable_days, avg_all (average units sold over all days) and avg_usable (average over usable days only), both averages rounded to one decimal. FILTER on an aggregate helps; it is covered in SQL you'll actually use.
CREATE TABLE daily_sales (store_id int, article_id int, sale_date date, units_sold int); CREATE TABLE stock_snapshot (store_id int, article_id int, snapshot_date date, on_hand_open int, on_hand_close int); INSERT INTO daily_sales VALUES (1, 101, '2026-07-01', 30), (1, 101, '2026-07-02', 32), (1, 101, '2026-07-03', 18), (1, 101, '2026-07-04', 31), (1, 101, '2026-07-05', 12), (1, 101, '2026-07-06', 29), (1, 202, '2026-07-01', 20), (1, 202, '2026-07-02', 22), (1, 202, '2026-07-03', 19), (1, 202, '2026-07-04', 25), (1, 202, '2026-07-05', 21), (1, 202, '2026-07-06', 24), (2, 101, '2026-07-01', 14), (2, 101, '2026-07-02', 9), (2, 101, '2026-07-03', 15), (2, 101, '2026-07-04', 16), (2, 101, '2026-07-05', 8), (2, 101, '2026-07-06', 15); INSERT INTO stock_snapshot VALUES (1, 101, '2026-07-01', 37, 7), (1, 101, '2026-07-02', 37, 5), (1, 101, '2026-07-03', 18, 0), (1, 101, '2026-07-04', 40, 9), (1, 101, '2026-07-05', 12, 0), (1, 101, '2026-07-06', 33, 4), (1, 202, '2026-07-01', 23, 3), (1, 202, '2026-07-02', 28, 6), (1, 202, '2026-07-03', 21, 2), (1, 202, '2026-07-04', 26, 1), (1, 202, '2026-07-05', 26, 5), (1, 202, '2026-07-06', 26, 2), (2, 101, '2026-07-01', 16, 2), (2, 101, '2026-07-02', 9, 0), (2, 101, '2026-07-03', 18, 3), (2, 101, '2026-07-05', 8, 0), (2, 101, '2026-07-06', 16, 1);
-- One row per store and product: all days, usable days, and both averages
SELECT store_id, article_id,
count(*) AS days,
round(avg(units_sold)::numeric, 1) AS avg_all
FROM daily_sales
GROUP BY store_id, article_id;
[{"store_id": 1, "article_id": 101, "days": 6, "usable_days": 4, "avg_all": 25.3, "avg_usable": 30.5}, {"store_id": 1, "article_id": 202, "days": 6, "usable_days": 6, "avg_all": 21.8, "avg_usable": 21.8}, {"store_id": 2, "article_id": 101, "days": 6, "usable_days": 3, "avg_all": 12.8, "avg_usable": 14.7}]
Show a solution
SELECT s.store_id, s.article_id,
count(*) AS days,
count(*) FILTER (WHERE k.on_hand_close > 0) AS usable_days,
round(avg(s.units_sold)::numeric, 1) AS avg_all,
round(avg(s.units_sold) FILTER (WHERE k.on_hand_close > 0)::numeric, 1) AS avg_usable
FROM daily_sales s
LEFT JOIN stock_snapshot k -- LEFT keeps days with no snapshot
ON k.store_id = s.store_id -- join on all three keys, or store 2's
AND k.article_id = s.article_id -- milk rows double up store 1's
AND k.snapshot_date = s.sale_date
GROUP BY s.store_id, s.article_id;A missing snapshot leaves on_hand_close NULL, and NULL > 0 is not true, so store 2's 4 July drops out of the usable count without any extra code. Look at store 1's milk: 25.3 units a day over all days, 30.5 over usable days. The two empty-shelf days pull the plain average down by about 17%, which is the censoring a model would learn. Even 30.5 may be low if the empty days were the busiest ones. Bread at store 1 never ran out, so both averages agree.
Frame "flag unusual partner orders"
The sales team asks for "AI to catch wrong partner orders". About 1,800 partner shops place about 4,000 orders a week. Some lines are mistakes: an extra zero, or cases typed as units. Today a rule flags a line when the quantity is more than 3 times the shop's 8-week median for that product. It flags about 40 lines a week, and when sales reps call the shops, about half turn out to be real mistakes. Write a short framing using the template sections: decision, prediction (unit, target, timing, task type), labels, metric pair, baseline, value and stop rule.
- You name the decision (a rep calls the shop before the DC picks the order) and who acts on the flag
- You define the unit (one order line) and the target (was it a mistake), and say whether it is classification with labels or anomaly detection without
- You say where labels come from (rep calls, corrections after delivery) and that unflagged lines are never checked, so their labels are missing
- You pair a model metric (precision of flags, mistakes found) with a business metric (lines corrected after delivery, rep time)
- You use the 3× median rule (about 40 flags a week, about half real) as the baseline, and say what a model must show to replace it
Show a model answer
Decision: a sales rep calls the shop to confirm a flagged line before the DC
picks the order. Owner: the sales team lead. Every order, every day.
Unit: one order line (shop, product, order). Target: was the line a mistake?
Timing: scored when the order arrives, before picking. Known then: the shop's
past orders, the product's pack sizes, the shop's 8-week median.
Task: classification if we keep the outcome of rep calls as labels; anomaly
detection while we have almost none.
Labels: rep call outcomes and corrections after delivery. Lines nobody flagged
are never called, so we only learn about mistakes the rule already catches.
Model metric: precision (share of flags that are real mistakes) and mistakes
found per week. Business metric: lines corrected after delivery, rep minutes.
Baseline: the 3x median rule, about 40 flags a week, about 20 real (50%).
Value: each mistake caught saves a return trip and a credit note.
Stop rule: if a model can't find clearly more mistakes for about 40 calls a week,
or keep the same catches with fewer calls, we keep the rule.The label problem is the one most people miss: only flagged lines get checked, so a model trained on today's labels mostly learns the rule back. A small random sample of unflagged lines checked each week fixes that. You will see in Explaining and choosing models how this case ends.
Check yourself
Marketing asks for "a churn model" for the app. What should you ask first?
A prediction is an input to a decision. Until you know whether it drives coupons, calls or nothing, you can't choose the target window, the metric or the threshold. Data volume and accuracy targets come after the decision is clear.
On the same 8-week backtest, Tomás's first model scores WAPE 33% against the 28-day average's 34%. Its bias is −8% and its festival-week WAPE is 57%. What is the best reading?
The model must beat the baseline by a margin worth running a new system, and it hasn't fixed the festival under-forecast where the money is. Shopping for a metric that flatters the model is the trap the agreed metric pair prevents, and a full rollout skips the pilot.
In a store pilot, the new forecasts for the products that run out most often keep coming in below what the stores could have sold. Which cause is most likely?
Products that run out often have the most censored labels, so a model trained on raw sales learns their demand too low, and the low orders make the shelves empty again. Overfitting shows up as noisy errors in both directions; a steady shortfall on the same products points to the labels.
Flashcards
Key takeaways
- A prediction is an input to a decision. Name the decision, its owner and its rhythm before choosing any data or model.
- Define the unit, the target and the horizon, and use only what is known at the moment of prediction.
- Sales are censored demand on empty-shelf days. Drop, down-weight or rebuild those labels, and write down which.
- Agree a model metric (WAPE and bias) and business metrics (stock-outs, waste, stock held) before seeing results; test offline, then in a pilot.
- Every model must beat today's rule on the same backtest by a margin worth the cost. Size the value roughly and write a stop rule.
Sources and further reading
- Rules of Machine Learning: Best Practices for ML Engineering, Martin Zinkevich, Google for Developers
- Supervised learning, scikit-learn user guide
- Forecasting: Principles and Practice, 3rd edition, Rob J Hyndman and George Athanasopoulos, OTexts
- Some simple forecasting methods (mean, naive and seasonal naive), Forecasting: Principles and Practice