Capstone: a sales alert you can defend
Put the track together: a week-by-week forecast, residuals one extreme week cannot distort, and precision, recall and cost to pick the limit with arguments.
Combines the 8 exercises of the Classic ML track. It unlocks when you finish them, but you can try it now.
WEEKLY holds 104 weeks of sales of a small food shop (synthetic, in EUR). Five weeks had a known incident (INCIDENTS): those are the alerts you want, and no others. Write five functions:
oneStepForecasts(xs, k): for each week t, the median of the k previous weeks (t - k … t - 1); null for the first k. The median, not the mean: one odd week must not spoil the next week's forecast.
robustZ(residuals): (r - median) / (1.4826 · MAD), where MAD is the median of |r - median|. When the MAD is 0, return zeros. It is the z-score from the invoice exercise, minus the pull of extreme values.
alerts(xs, k, limit): residual = actual - forecast for the weeks that have a forecast; return the week indexes whose |z| is above limit.
evaluate(flagged, incidents): { tp, fp, fn, precision, recall } of the alerts against the known incidents, 0 when a denominator is 0.
pickLimit(xs, incidents, k, limits, costFP, costFN): { limit, cost } for the limit in limits with the lowest fp · costFP + fn · costFN; on a tie, the highest limit (fewer alerts).
The preview draws the weeks, your rule's alerts and the real incidents, and lets you move the limit and the relative cost.
Challenges 0/5
- oneStepForecasts: the median of the k previous weeks, null at the start
- robustZ: median and MAD, and the extreme value hides nobody
- alerts with k = 4 and limit 3 finds four of the five incidents
- evaluate: hits, false alarms, missed incidents, precision and recall
- pickLimit follows the cost: an expensive missed incident lowers the limit
function oneStepForecasts(xs, k) {
// for week t: the median of weeks t - k … t - 1; null for the first k weeks
return xs.map(() => null);
}
function robustZ(residuals) {
// (r - median) / (1.4826 * MAD), where MAD is the median of |r - median|; all zeros when the MAD is 0
return residuals.map(() => 0);
}
function alerts(xs, k, limit) {
// residual = actual - forecast for the weeks with a forecast; the week indexes whose |z| is above limit
return [];
}
function evaluate(flagged, incidents) {
// tp, fp, fn, precision, recall of the flagged weeks against the known incidents (0 when a denominator is 0)
return { tp: 0, fp: 0, fn: 0, precision: 0, recall: 0 };
}
function pickLimit(xs, incidents, k, limits, costFP, costFN) {
// the limit with the lowest fp * costFP + fn * costFN; on a tie, the highest limit (fewer alerts)
return { limit: limits[0], cost: 0 };
}
console.log(alerts(WEEKLY, 4, 3), pickLimit(WEEKLY, INCIDENTS, 4, LIMITS, 1, 5));The idea
This is what a small business usually means by "I want the AI to warn me when something odd happens". It does not need a large model: it needs a simple forecast, a measure of how far each week strays that one odd week cannot distort, and an explicit decision about how many false alarms you accept.
The median of the previous four weeks as the forecast has one virtue: the broken-cold-room week does not contaminate the next week's forecast. With the mean it would, and the week after a dip would be flagged as a spike. The same goes for the MAD against the standard deviation: the 1771 EUR week does not inflate the scale, and the other four incidents stay visible.
The limit is not picked by eye. With the five known incidents you can measure precision and recall for each limit, put a cost on every false alarm and every missed incident, and keep the cheapest. Here, limit 3 raises no false alarm but misses the power cut; limit 2 finds all five at the price of three extra warnings. That table is what you show when someone asks why the system warned, or why it did not.
Five incidents in two years are too few to trust the exact number much. That is an honest limit: the procedure holds, and the figure improves with every year of data.
Run it on your machine
With Python and pandas, on this exercise's CSV (CC BY 4.0). The same rule in ten lines:
curl -O https://vorluxai.com/data/labs/ventas-semanales-incidencias.csv
python3 -c "
import pandas as pd
df = pd.read_csv('ventas-semanales-incidencias.csv')
forecast = df.ventas_eur.shift(1).rolling(4).median()
res = df.ventas_eur - forecast # keep the index aligned; NaN rows never alert
z = (res - res.median()) / (1.4826 * (res - res.median()).abs().median())
print(df[z.abs() > 3][['semana', 'ventas_eur', 'incidencia']])
" Go deeper: the Forecasting: Principles and Practice reference →
This in production, with your data? Let's talk for 15 minutes →