Commandment 4 – “Honour Predictive AI and Automation as Thy Elders”

Part 5 of the series The 10 Commandments of AI in Business: Choosing the right intelligence for the right problem.

1.0 A recap

Commandment 3 was a defence of the humble. We argued that the single well-crafted prompt and the fixed chain quietly solve the majority of real generative AI problems in business, and that with a little engineering discipline, structure, validation, evals, they are both the cheapest and the most trustworthy thing you can put into production. We ended on a warning about a different kind of overreach:

“A great many problems people throw generative AI at aren’t language problems at all. They’re prediction problems, or plain automation.”

So here we are.

Commandments 2 and 3 lived entirely inside the generative world. This commandment steps outside that world and asks the prior question the whole industry keeps skipping:

Should this be generative AI at all? Should it be AI at all?

Because there is an even more unfashionable set of tools than the humble prompt. They are the elders: machine learning, statistical forecasting, rules engines and robotic process automation (RPA). They are decades old. They are boring. And they are frequently more accurate, cheaper, faster, and auditable than the shiny generative thing a team is about to build on top of them. This commandment is a plea not to disown them merely because they no longer trend on LinkedIn.

1.1 The generative costume

Here is the pattern that costs organisations dearly right now. A real, well-understood problem, the kind with decades of proven technique behind it, arrives on someone’s desk. And because “AI” now means “generative AI” in the popular imagination, the team reaches for a large language model, or worse, an autonomous agent, to solve it.

Consider the now familiar story. A retailer decides to “use AI to improve demand forecasting.” A team spends four months and a substantial token bill building a “forecasting agent”, an LLM that ingests sales data, reasons about seasonality in natural language, and produces a forecast for each product. It demos beautifully: you can ask it why it predicted what it did, and it answers in fluent prose. But in production it is slow, expensive per forecast, inconsistent from run to run, and its accuracy on the SKUs that actually matter is mediocre. Worse, its forecasts don’t reconcile: the numbers it generates for individual products don’t add up to the numbers it generates for the category or the region.

Meanwhile, the problem it was built to solve has a mature, well-understood answer: hierarchical time-series forecasting, a discipline with decades of rigour behind it. A proper hierarchical model forecasts at every level, SKU, category, region, national, and mathematically reconciles them so the parts always sum to the whole. It runs in seconds across the entire catalogue, costs a fraction of a penny per forecast, produces the same answer every time, and is more accurate on exactly the items that drive the business.

They built a fashionable forecasting agent for a problem that time-series statisticians solved long ago. And the tragedy is that the agent almost worked, which is precisely what makes this mistake so expensive: the demo was fluent and impressive enough to get funded, and the failure, the cost, the inconsistency, the forecasts that wouldn’t add up, only showed up at production scale.

Type of intelligenceWhat it produces ?Example
Generative AINew content : text, drafts, summaries, code, imagesDraft a reply to this customer
Predictive AIA number, class, or ranking learned from historical dataWill this customer churn?
Automation / rules / RPA / queryA deterministic output when the logic is knownFlag any invoice over £10,000 from a new supplier

The failure mode of the era is reaching for the first row when the problem clearly lives in the second or third.

1.2 Recognising a predictive problem in disguise

Commandment 3 equips business leaders with a structured framework of patterns to instantly spot and define predictive opportunities. If the desired output is a score, a category, a forecast, or a ranking, the problem is almost certainly predictive, not generative.

Let us learn to recognise this family and you will spot the “let’s build a GenAI system for it” hype immediately:

None of these produce new content. They produce a number or a class from patterns in data you already have. That is not the job, a language model was built for.Asking it to do that job means paying more for a less accurate, less explainable answer.

The legitimate exception: a generative wrapper around a predictive core

So far this reads as “keep generative AI away from predictive problems.” That’s too blunt, and it misses one of the most genuinely useful ways the two combine. There is a legitimate and increasingly common role for generative AI here, and it’s worth understanding precisely, because it’s also easy to overdo.

The pattern is this: the predictive model still makes the decision; a generative layer sits on top of it purely as an interface. The churn model still produces the score. The forecasting model still produces the numbers. But instead of that output landing in a dashboard nobody opens, a generative, sometimes agentic, wrapper lets a human converse with it:

  • “Which of my accounts are most at risk this month, and why?”
  • “What happens to the regional forecast if I run a 10% promotion in March?”
  • “Show me the three biggest drivers behind this customer’s fraud score.”

The predictive model computes; the generative layer translates that computation into natural language, fields follow-up questions, and, in the agentic version, orchestrates the calls (fetch the score, retrieve the drivers, run the what-if scenario). This is how generative AI is honestly incorporated into the predictive fold, not as a replacement for the model, but as a conversational skin over it. Note that the wrapper is the communication layer, dressed up as a chat interface.

1.4 But does every predictive problem need one?

This is the discipline that stops the pattern becoming another force-fit. A generative wrapper is an interface, and interfaces cost money and add moving parts. The question is not “can we build a chat interface for this?”, it’s “does this use case actually warrant one?”

Most predictive systems don’t. A fraud model scoring millions of transactions per second needs no conversation, it needs a threshold and an automated action. A demand forecast that feeds directly into an automated replenishment system has no human in the loop to talk to. Bolting a chat interface onto these adds cost and fragility for an audience that doesn’t exist.

A generative or agentic wrapper earns its place only when a human genuinely needs to interrogate, explore, or act on the prediction in a flexible, unscripted way. Some use cases where it is warranted:

  • A relationship manager working a churn list. They don’t want a raw score; they want to ask “why is this account at risk, and what’s worked for similar accounts before?” and then have a draft outreach written for them. The exploration is open-ended, so a conversational layer adds real value.
  • A planner running what-if scenarios on a forecast. The value is in the dialogue, “what if the promotion slips two weeks?”, “what if the supplier is late?”, which a fixed dashboard can’t anticipate but a generative interface over the model can.
  • An analyst investigating a flagged fraud case. A human is already in the loop for high-value flags; letting them ask the model to summarise the drivers and pull related transactions in natural language speeds a genuinely human, judgement-heavy task.

The common thread: a wrapper is justified when there’s a human decision-maker doing open-ended, exploratory work on top of the prediction. Where the prediction feeds an automated action at scale, skip the wrapper entirely, it’s cost without a customer.

So the rule extends cleanly: the predictive elder owns the decision, always. Generative AI may serve as its interface, but only when a human genuinely needs to converse with the answer, and never as the thing computing it.

1.3 When the right amount of AI is none

There is a rung below predictive AI, and it is the most overlooked of all. Sometimes the correct answer is no model whatsoever:

  • A rule, when the logic is known and stable. “Route invoices over £10,000 from new suppliers to manual approval” does not require intelligence. It requires an if statement.
  • RPA, when the task is repetitive, structured, and rule-bound — moving data between two systems, reconciling fields, filling forms. No model, no hallucination risk, fully deterministic.
  • A database query, when the “insight” someone is asking an AI to generate is really just an aggregation. “Which region underperformed last quarter?” is a GROUP BY, not a neural network.

The mindset shift, in the spirit of Commandment 3’s “what’s the simplest thing that reliably works?”: before reaching for any model, ask whether the logic is actually knowable and fixed. If it is, a rule beats a prediction, and a prediction beats a generation, on cost, speed, and trust every single time.

2.0 Three business processes, and where the elders belong

Let’s honour the recurring principle of this series. Look at the actual process, step by step, and place the tool where the nature of the work belongs. In each case below, watch a recurring pattern emerge: the predictive or deterministic elder owns the decision; generative AI, if it appears at all, is confined to the language wrapped around that decision.

Example A: Customer retention (the churn trap)

The seductive framing: “Let’s use AI to understand our customers and reduce churn.”

The process:

Collect customer signals >> Identify who is at risk >> Understand why >> Reach out to save them

The instinct is to build “a GenAI churn assistant.” But look at the actual work:

StepNature of the taskCorrect tool
1. Score each account’s churn riskPrediction from historical dataClassification model (the elder)
2. Explain the driver per accountFeature importanceModel output, fully explainable
3. Prioritise the outreach listRankingSort the scores
4. Draft personalised outreachNew contentGenerative AI (the youngster)

Only the last step is a language problem. Steps 1–3 are a textbook supervised-learning job, more accurate, near-free per prediction, and defensible to anyone who asks “why was this customer flagged?” Once the model decides who and why, a humble drafting workflow (Commandment 3) handles the words. The elder makes the decision; the youngster writes the email. Put GenAI in charge of the prediction itself and you get a system that is slower, pricier, less accurate, and unable to explain itself.

Example B: Accounts payable (the automation trap)

The seductive framing: “An AI agent that manages our invoices end to end.”

The process:

Receive invoice >> Read it >> Match to purchase order >> Validate totals >> Route for approval >> Pay

StepNature of the taskCorrect tool
1. Read fields from a structured invoiceDeterministic parsingTemplate / OCR
2. Read fields from an unstructured supplier emailExtractionGenerative AI (narrow role, per Commandment 3)
3. Match invoice to purchase orderDeterministic lookupRules + query
4. Validate totals, tax, datesArithmetic and rulesRules engine
5. Route for approvalDeterministic routingRPA / workflow rule

Roughly 90% of this process is deterministic. Matching an invoice to a purchase order is not an act of intelligence, it is a join between two tables. Validating that line items sum to the total is arithmetic. Routing based on amount is a rule. There is exactly one sliver where generative AI earns its place: reading the occasional messy, unstructured supplier email and extracting fields from it, and even that hands its output straight into the deterministic pipeline for validation. Build an “agent” for this and you have introduced hallucination risk and unpredictability into a process whose entire value is that it is auditable and correct by construction.

Example C: Demand planning (the forecasting trap)

The seductive framing: “Let’s ask the LLM how much stock we’ll need next quarter.”

The process:

Gather sales history >> Account for seasonality & trend >> Forecast demand >> Brief the regional managers

StepNature of the taskCorrect tool
1. Forecast demand per SKU per regionTime-series predictionStatistical / ML forecasting (the elder)
2. Adjust for known events (promotions, holidays)Rules + featuresModel features
3. Turn the forecast into a readable narrative briefNew contentGenerative AI (the youngster)

Time-series forecasting is a discipline with decades of rigour behind it. A proper forecasting model is more accurate, cheaper, and far more defensible than a language model’s plausible-sounding guess about a number it has no basis to know. Asking an LLM “how much stock will we need?” is asking a system with no grounding in your sales data to invent a figure. The generative sliver, again, is purely communication: once the model has produced the forecast, GenAI can turn a table of numbers into a clear narrative brief for each regional manager. It communicates the answer. It does not compute it.

The recurring pattern across all three: predictive AI and automation do the decision; generative AI does the language around the decision. Force-fitting GenAI onto the decision itself is where accuracy, money, and auditability go for a toss.

2.1 The advantages the elders still hold

This is not nostalgia. The older techniques retain concrete, measurable advantages for the problems they own:

DimensionPredictive AI / AutomationGenerative AI
Accuracy on structured problemsHigh and measurableOften lower, harder to pin down
Cost per decisionFractions of a pennyMeaningfully higher (tokens)
Speed / latencyMillisecondsSeconds
ExplainabilityFeature weights, clear logicOpaque reasoning
AuditabilityFully traceableDifficult to defend to a regulator
ConsistencyDeterministic or stableProbabilistic, can drift

For a regulated decision, credit, fraud, anything a customer can appeal or an auditor can challenge, that explainability and auditability column is not a nice-to-have. It is often a legal requirement. A generative system that cannot cleanly answer “why did you decide this?” is not merely inferior here; it may be unusable.

2.2 A field guide to spotting a generative force-fit

Mirroring Commandment 3’s warning signs, here are the patterns that a team has dressed a predictive or deterministic problem in a generative costume. If you spot two or more of these in a proposal, stop and reconsider the tool:

  • The output should be a number or a category, but you’re generating a paragraph to arrive at it.
  • You have an eval set (Commandment 3) and accuracy has plateaued well below what a simple model reportedly achieves on the same task.
  • The unit economics only work at demo scale. The cost per transaction is fine for ten examples and alarming at ten million (a direct foreshadow of Commandment 5).
  • An auditor or regulator would ask “why did the system decide this?” and you have no clean, defensible answer.
  • The task never changes, yet you’ve built something probabilistic to do it. Fixed logic deserves deterministic tools.
  • You have years of clean, labelled historical data sitting unused while an LLM guesses at a problem that data could answer directly.

3.0 The decision lens: which elder (or youngster) does this job need?

When a GenAI proposal lands on your desk, run it through this before approving anything:

Ask thisIf the answer points here…Then reach for…
What is the output : new content, or a number/class/ranking?A score, class, forecast, or rankingPredictive AI, not generative
Is the logic known and stable, or must it be learned from data?Known and stableRules / RPA / a query — no model at all
Do we have labelled historical data for this?Yes, plentyPredictive AI is likely viable and superior
Must the decision be explained or audited?YesThe elders have a decisive edge
Is there a genuine language task hidden in the process?Yes — a draft, summary, or extractionGenerative AI, confined to that step only

Rule of thumb: Before anyone builds a generative system, prove the problem is genuinely generative. Name the output first. If it’s a score, a class, a forecast, or a ranking, you have a predictive problem, and the unfashionable elder will very likely be more accurate, cheaper, faster, and more defensible.

3.1 The business leader’s checklist

The next time a team pitches an “AI” build, ask:

  • ☐ “What exactly is the output, a piece of new content, or a number/category/ranking?” This one question sorts generative from predictive faster than anything else.
  • ☐ “Did anyone check whether a simple rule, an RPA bot, or a database query already solves this?” Make no-model the default the model must beat.
  • ☐ “Are we paying generative prices for a predictive result?” Tie it to the unit economics, not the demo.
  • ☐ “Can we explain and audit this decision if a customer or regulator challenges it?” If not, an opaque generative model may be a liability, not an asset.
  • ☐ “Have we confined GenAI to the language layer, and left the decision to a proven technique?” The elder decides; the youngster communicates.
  • ☐ “Are we choosing generative AI because it’s right for this problem, or because it’s fashionable?” Be honest about which one is driving the design.

If the honest answers are “the output is a churn score, we have five years of labelled data, a classification model does it for a fraction of a penny with full explainability, and GenAI only drafts the outreach email once the model has flagged the account”. These are use cases where you’ve honoured your elders. Ship the predictive model for the decision, use the humble workflow for the words, and put the fashionable option back on the shelf until a problem actually needs it.

4.0 Conclusion

There is a quiet prejudice in every organisation right now, and it is the mirror of the one we named in Commandment 3. There, complexity was mistaken for competence. Here, novelty is mistaken for capability. Generative AI is new and exciting, so it gets reached for reflexively, while the techniques that would actually solve the problem, forecasting, classification, rules, RPA, are dismissed as old-fashioned plumbing.

But the age of a technique tells you nothing about its fitness for the job. A logistic regression from the 1950s will out-predict a state-of-the-art language model on a churn problem, for a fraction of the cost, with an explanation attached. A rules engine will process invoices without ever inventing a number. These tools are not the embarrassing past we’ve moved beyond. For the enormous class of predictive and deterministic problems that make up much of real business, they are still the correct answer.

Honour predictive AI and automation as thy elders. They do more of the real decision-making than anything with a fancier name, and they do it more cheaply, more quickly, and more defensibly than the costume-wearing generative alternative ever will.

Next up: Commandment 5 – “Thou Shalt Not Slay Thy Token Budget.”

We’ve now covered which tool to reach for, the humble workflow (Commandment 3) and the predictive elders (Commandment 4). But even when you’ve correctly chosen generative AI for a genuine language task, you can still bankrupt the project with a heavyweight implementation. In the next post we turn to cost discipline: why grounding and retrieval deliver the result for a fraction of the compute, why bigger models and bloated context are so often needless extravagance, and how to get the right answer without setting fire to your token budget. Commandment 4 kept you from using the wrong tool. Commandment 5 keeps you from using the right tool wastefully.

Commandment 3 – “Honour the Humble LLM Workflow”

Part 4 of the series The 10 Commandments of AI in Business: Choosing the right intelligence for the right problem.

1.0 Where we left off

Commandment 2 spent a lot of words and one experiment telling you what not to build. We proved that for a known, repeatable process, an agent costs up to 7.4× more, forever, for identical output. We ended on a promise,

“You don’t have an agent problem. You have a workflow which, conveniently, is the subject of the next commandment.”

So here we are. If most business processes are known roads that don’t need an agent behind the wheel, the obvious question is: what should we be driving instead?

The answer is deeply unglamorous, and that’s exactly why it gets overlooked. It’s the single well crafted prompt and the fixed chain of prompts ,the boring, cheap, reliable workhorse that quietly solves the majority of real GenAI problems in business. This commandment is a defence of the humble. Because in the rush to look cuttingedge, teams skip straight past the tool that would have worked.

1.1. The two humble workhorses

Recall the decision lens and hierarchy ladder from Commandment 2. The current commandment lives entirely on the bottom two rungs, the ones everyone is embarrassed to build or state publicly, because they don’t sound impressive in a stand-up.

RungWhat it is ?When it wins ?
Multi agentCommandment 2Rarely
Single agentCommandment 2Only when the path is genuinely unknown
Fixed chainA pre-defined sequence of prompts, wired together by youA multi step task where you know the steps in advance
Single promptOne LLM call: instruction in, result outA single self contained task

The critical distinction from an agent is that, in a workflow, the developer owns the path. You decide step 1 >> step 2 >> step 3. The LLM never asks “what should I do next?”. It only fills in the blank you hand it. No reasoning loop, no growing context, no runaway tokens, no unpredictability. Just a dependable pipeline.

The mindset shift for leaders: stop asking “how do we make this AI impressive?” and start asking “what’s the simplest thing that reliably works?” The measure of a good AI implementation is not its sophistication. It’s the ratio of business value to complexity and the humble workflow wins that ratio far more often than anyone admits.

1.2. The six highest-ROI, lowest-risk patterns

Here’s the good news that the hype obscures. A huge share of everyday business processes collapses into just six patterns, and every one of them is a single prompt or short chain job. Learn to recognise these, and you’ll spot the “why don’t we build an agent?” over reach immediately.

PatternWhat it doesTypical business taskWhy it’s low-risk
SummarisationLong >> shortCondense a contract, a call transcript, a reportOutput is checkable against the source
ExtractionUnstructured >> structuredPull order ID, dates, amounts from an emailOutput can be validated against a schema
ClassificationInput >> a labelRoute a ticket to billing/technical/salesFinite, testable set of answers
DraftingBrief  >> first draftReply, job description, product blurbA human edits before it ships
TranslationLanguage A >> language BLocalise copy, translate a queryMature, well-benchmarked capability
ReformattingFormat A >> format BNotes >> structured minutes; prose >> tableDeterministic and easy to verify

Notice the common thread in the final column. In all six cases, verifying the output is both cheap and fast. That low verification cost is precisely what minimizes risk. While AI evangelists in your company might rush to “agentise” these patterns, using an agent here is almost always overkill. You aren’t asking the model to make high stakes, autonomous decisions. You’re simply asking it to transform text that a human or automated validator can instantly double check. That makes these tasks the natural domain of simple, humble workflows, which already account for a massive chunk of real business operations.

2.0. Three business processes, and where the humble workflow fits

Let’s honour the recurring principle of this series i.e. Look at the actual process, step by step, and place the tool where the nature of the work belongs.

Example A : Contract summarisation (a single prompt, then a small chain)

The process:

Receive contract >> Read & understand >> Identify key terms >> Summarise obligations & risks >> Flag anything unusual >> Route to the right reviewer

The naive instinct is “build a contract analysis agent.” But look at the path, it never changes. Every contract goes through the same steps in the same order. You can draw the flowchart. So it’s a fixed chain, not an agent:

StepPatternImplementation
1. Extract key clauses (term, liability, termination, payment)ExtractionOne prompt >> structured output
2. Classify each clause’s risk (standard / review / red-flag)ClassificationOne prompt per clause type
3. Summarise obligations in plain EnglishSummarisationOne prompt
4. Route red-flags to legalAutomationA rule on the classifier output

Three fixed prompts and a routing rule. No agent. Cheaper, faster, predictable and every step’s output is verifiable against the source contract. The human lawyer stays the accountable reviewer (a nod back to Commandment 1‘s HR-grievance lesson: keep judgement human).

Example B : Ticket triage (you’ve already seen this one win)

This is the exact task we benchmarked in Commandment 2 and it’s the poster child for the humble workflow:

Classify >> look up >> draft >> route.

We proved a fixed 3-call chain did it at 1/7th the cost of the multi-agent version, for identical output. I won’t re-litigate it. I’ll just point at it as Exhibit A. When a process is this predictable, the humble chain isn’t the compromise. It’s the correct answer.

Example C : Meeting-note extraction (a single prompt does most of it)

The process:

Record meeting >> Transcribe >> Extract decisions & action items >> Assign owners & due dates >> Format as minutes >> Distribute

StepPatternImplementation
TranscribeSpeech-to-text. Not an LLM jobA transcription service
Extract decisions, actions, owners, datesExtractionOne structured prompt
Format as minutesReformattingSame prompt or a template
DistributeAutomationA rule / integration

The entire “intelligent” part is one well designed extraction prompt. No chain, no agent. This is the kind of task where a team spends three weeks architecting an “agentic meeting assistant” when a single prompt with a good schema would have shipped on day one and worked more reliably.

2.1. The catch: “simple” is not the same as “reliable”

Here’s the honest part, and the reason this commandment isn’t just “use fewer tokens.” A humble workflow is only valuable if it’s trustworthy in production, and a lasily written prompt is not. The gap between a demo prompt and a production workflow is exactly where most simple GenAI projects quietly fail.

The difference comes down to three engineering disciplines. None of them are glamorous, and all of them matter more than the model choice:

1. Structured outputs. Don’t let the model reply in free prose and then try to parse it with fragile string matching. Force it to return a defined schema (JSON with named fields), using the model’s structured-output / function-calling feature. Now the output is machine-usable and “validatable” by design.

2. Templates. Don’t hand-write a fresh prompt each time. Build a prompt template with slots for the variable parts (the contract, the email, the transcript) and a fixed, tested instruction around them. This is what makes the workflow repeatable the same instruction runs on every input.

3. Validation (and retry). After the model responds, check it before trusting it: did it return valid JSON? Are all required fields present? Are dates real dates, amounts real numbers? If validation fails, retry once with the error fed back. This one cheap loop turns a flaky prompt into a dependable component.

The principle: an agent gets its reliability from the model reasoning its way out of trouble (expensively, unpredictably). A humble workflow gets its reliability from you engineering the trouble out in advance, cheaply, and deterministically. That’s not a downgrade. It’s often the more professional choice.

3.0. A small experiment on reliability, not cost

In Commandment 2 we measured cost. It would be lasy to run the same experiment again. But there’s a different axis worth measuring here and it directly answers the  “is simple actually trustworthy?”.

The setup:

An extraction task (pull order_id, account_email, and issue_type from support messages), run many times, two ways:

  • Design A : Naive prompt: “Extract the order id, email and issue from this message.” This is a Free-text reply.
  • Design B : Engineered workflow: the same task, but with a forced JSON schema + validation + one retry on failure.

We measure not dollars, but parse success rate (did we get valid, complete, machine-usable output?) and consistency(same input >> same output across runs).

Batch of 4 messages × 5 run(s) each.

MetricA. Naive promptB. Engineered workflow
Valid, complete output8%100%
Consistent across runs12%100%
Downstream integrationFragile string parsingClean, typed fields
Cost per callsamesame

The full runnable script is available on request; I’d encourage running it on your own data, as the naive failure rate depends heavily on your inputs.

The lesson is the mirror-image of Commandment 2. There, the agent’s unpredictability (the ± spread) was the villain. Here, the villain is a lasily-built simple prompt and the hero is a well-engineered one. The humble workflow is not automatically reliable; it becomes reliable when you add structure and validation. And crucially that reliability costs you nothing extra in tokens. It’s pure engineering discipline, not more compute.

This reframes the whole commandment: the choice isn’t “cheap-but-flaky simple workflow” vs. “expensive-but-smart agent.” Done right, it’s “cheap and reliable workflow” vs. “expensive, unpredictable agent.”

For the six patterns above, the humble option wins on both axes.

4.0. Guardrails and lightweight evaluations

One more discipline separates a workflow you can trust from one you merely hope works. Leaders should insist on it before any GenAI goes into a real process:

  • A small “eval set.”: Take 20-30 real examples with known-correct answers. Every time you change the prompt or the model, run it against this set and measure accuracy. This is the single cheapest insurance policy in AI, and astonishingly few teams do it.

Example : Support ticket classification

A SaaS company routes inbound tickets into five categories: Billing, Bug, Feature Request, Account Access, Other. They, pull 80 historical tickets that a senior support lead has already labelled correctly. This becomes the frozen eval set. Each entry is simply:

Ticket textCorrect label
“My card was charged twice this month”Billing
“The export button does nothing on Safari”Bug
“Can you add dark mode?”Feature Request

When someone tweaks the classification prompt, they run all 80 and get a number: “91% match, up from 87%.” That’s a decision they can defend one which is validated by a proper eval set

  • Guardrails on the output. Simple checks the workflow enforces automatically: is the classification one of the allowed labels? Is the summary within length? Does the extraction contain no invented fields? Reject and retry if not.

Example: Allowed-label check (classification)

allowed = {"Billing", "Bug", "Feature Request", "Account Access", "Other"}

if model_output not in allowed:

    retry()          # e.g. the model returned "Payment Issue"

The model occasionally invents a label like “Refund”. The guardrail rejects anything outside the five permitted values and forces a retry, so a rogue category can never reach the routing system.

  • A human checkpoint sized to the stakes. Low-stakes, easily-reversed output (e.g. spot check a draft email). High-stakes or irreversible (e.g. human sign-off, always  for a contract clause sent to a client) . This scales the human-in-the-loop idea from Commandment 1.

None of this requires a data-science team or a fancy platform. It requires the discipline to treat a “simple” workflow as a real production component because it is one.

5.0. The decision lens: is this a humble workflow job?

When a GenAI proposal lands on your desk, run it through this before approving anything more complex:

Ask thisIf YES If NO 
Is the task one of the six patterns (summarise / extract / classify / draft / translate / reformat)?Single prompt or chainLook harder before escalating
Can I check whether the output is correct, cheaply?Low-risk : proceedHigher-risk : add human review / evals
Is the sequence of steps known in advance?Fixed chain (not an agent)Then consider an agent (Commandment 2)
Is the output structured and validatable?Production-ready patternAdd structured output + validation first
Does a wrong answer get actedon automatically?Insert a human checkpointAutomate the routing

Rule of thumb: Before anyone builds an agent, prove that a single prompt and a fixed chain can’t do it. In the majority of business tasks, they can and they’ll be cheaper, faster, and more reliable.

5.1. The business leader’s checklist

The next time a team pitches a GenAI build, ask:

  • ☐ “Which of the six patterns is this really?” : Most business tasks are one of them.
  • ☐ “Can we do this with a single prompt or a fixed chain first?” : Make the simpler option the default, and the complex one justify itself.
  • ☐ “Is the output structured and validated or are we parsing free text and hoping?” : Desired for production reliability.
  • ☐ “Where’s the eval set?” : If they can’t show you 30 examples with known answers, it isn’t production-ready.
  • ☐ “What happens when it’s wrong, and who checks?” : Making human judgement a supreme gate.
  • ☐ “What does this cost per transaction versus the fancier alternative?” : Tie it back to Commandment 2. Simple usually wins on cost and reliability.

If the honest answers are “it’s an extraction task, one prompt with a schema does it, here’s our eval set, and legal signs off on red-flags” :

Congratulations!!!

You have a humble workflow. Ship it, and put the engineering effort into reliability, not sophistication.

6.0 Conclusion

There is a quiet bias in every organisation right now. Complexity is mistaken for competence. Building an agent feels like real AI work. Writing one excellent, structured, validated prompt feels like… not much. So teams routinely over-build and end up with something more expensive, slower, and less reliable than the humble alternative would have been.

This commandment is a plea to resist that bias. The single prompt and the fixed chain are not the consolation prize you settle for when you can’t afford an agent. For the vast majority of business language tasks, summarising, extracting, classifying, drafting, translating, reformatting: they are the right answer, and done with a little engineering discipline (structure, validation, evals), they are both the cheapest and the most trustworthy thing you can put into production.

Honour the humble LLM workflow. It does more of the real work than anything with a fancier name.

Next up : Commandment 4: “Honour Predictive AI and Automation as Thy Elders.”

We’ve now defended the simple generative workflow. But there’s an even more overlooked truth. A great many problems people throw generative AI at aren’t language problems at all. They’re prediction problems, or plain automation. In the next post we rehabilitate the unfashionable elders : machine learning, forecasting, and old-fashioned rules and show how to spot a “predictive problem wearing a generative costume.”

Commandment 2 – “Thou Shalt Not Invoke Agents in Vain”

Part 3 of the series The 10 Commandments of AI in Business: Choosing the Right Intelligence for the Right Problem.

1.0 First, a quick recap

In Commandment 1, we established that the process is sovereign . You start with the process, find the constraint, and let the nature of the work at the constraint choose the tool. We had a six-row decision lens, and we noticed that AI Agents were just one row in it reserved for the specific job of coordinating multiple systems.This post zooms into that one row, because it is the single most abused word in business AI today.

Somewhere in your organisation, right now, someone is proposing to solve a problem with “an agent.” Maybe a “multi-agent system.” It sounds sophisticated. It sounds like the future. And in perhaps one case out of five, it’s the right call.

This commandment is about identifying that one case from the other four

2.0 The scene (you’ve lived this one too)

A process is running slowly. Someone has read that agentic systems are the frontier of AI. They propose:

“Let’s build an agent for it. Actually a multi-agent system. One agent to read the request, one to look up the data, one to draft the reply, one to check it.”

It sounds like a well-organised team of digital workers. What it actually is, in most cases, is a slow, expensive, unpredictable way to do something a fifty-line script could have done reliably for a fraction of the cost.

To understand why, we first have to be honest about what an agent actually is because the word is doing a lot of marketing work that the technology doesn’t always earn.

3.0 The same brain, with a different packaging

Here is the most important thing a business leader can understand about agents, and almost nobody says it plainly:

An agent is not a smarter AI. It is the same LLM — the same “brain” — wrapped in scaffolding that lets it act on its own.

The large language model at the centre of an agent (Claude, GPT, Gemini) is exactly the same model you use when you type a single question into a chat window. It is not more intelligent because you called it an “agent.” It has the same knowledge, the same reasoning ability, and the same tendency to make mistakes.

What changes is the harness built around it. An agent is an LLM plus:

  • Tools — the ability to call external functions (search a database, send an email, query an API).
  • A loop — permission to run itself repeatedly until it decides the job is done.
  • State & memory — a scratchpad of what it has done so far and what it has learned.
  • Autonomy over the path — and this is the crucial one.

That last point is the whole distinction. Let me make it concrete.

In a simple prompt or a workflow, you decide the steps. You write: “First classify the email -> then extract the fields -> then draft a reply.” The LLM fills in each blank, but the path is fixed, predictable, and controlled by you.

In an agent, the LLM decides the steps. You give it a goal, “resolve this customer’s issue” and a set of tools, and it decides what to do first, what to do next, when to use a tool, and when it’s finished. You have handed the steering wheel to the model.

That is the trade at the heart of this commandment:

WorkflowAgent
Who controls the path?You (fixed)The LLM (dynamic)
Predictable?YesNo
Cheap & fast?YesNo
Good when the steps are…Known in advanceUnknown in advance

You hand over the steering wheel only when you genuinely don’t know the road in advance. Most business processes are known roads. That is why most business processes do not need an agent.

4.0. The Anatomy of an agent — how it actually works ?

Before we look at applications of an agent or even cost an agent out, we need to demolish the biggest myth about it: that the agent magically knows how to look things up, send emails, or issue refunds. It doesn’t. Every one of those abilities is a tool that a developer built, described, and handed to the LLM in advance. And the “memory” that lets it string steps together isn’t memory at all, it’s scaffolding re-reading a transcript to a model that forgets everything between calls. Let’s take these one at a time.

4.1 What exactly is a “tool”?

A tool is simply a capability you give the LLM so it can act on the outside world instead of just talking about it. On its own, an LLM can only produce text.It cannot look up your billing records or send an email. A tool is the bridge. In practice, a tool is almost always one of these:

  • A function you write — a small piece of code that calculates a shipping cost or formats a date.
  • A database query — “fetch this customer’s last 10 transactions.”
  • An API call to another system — your payment gateway, your CRM, a weather service, a Slack message.
  • A search — over the web, or over your internal documents.

The critical point to note here is that none of these appear by magic. Someone in the team has to build each one, connect it securely to the underlying system, and maintain it. Ten tools means ten integrations to build, test, secure, and keep working when the underlying systems change. That is real, recurring engineering cost — and it’s part of the price tag of “just building an agent.”

4.2 How a tool is defined and how the LLM knows what it does ?

Here’s the part that removes the mystery. When you give a tool to an LLM, you don’t just hand it the code. You register it with a plain-language description and a defined set of inputs. Conceptually, each tool is declared like this:

FieldDefinition
Tool nameget_billing_history
Description“Retrieves all charges for a given customer in the last 90 days. Use this when you need to see what a customer was actually billed.”
Inputscustomer_id a number (required)
ReturnsA list of charges, each with a date, amount, and status

You define, up front a toolbox, the complete set of tools, the agent is allowed to use for this job. For a billing-dispute agent, that toolbox might be:

ToolWhat its description tells the LLMUnderlying reality
get_billing_history“Use to see a customer’s charges”A database query
get_gateway_logs“Use to check if a payment was retried or failed”An API call to the payment provider
issue_refund“Use to refund a specific charge”An API call that moves real money
send_email“Use to message the customer”An API call to your email system

4.3 So how does the LLM decide which tool to use?

This is the question everyone should ask, and the answer is refreshingly un-magical:

The LLM uses the tool descriptions, names, parameter schemas, and surrounding context to determine which tool is most appropriate. It’s matching intent to tool metadata the same way you’d scan a toolbox and reach for the screwdriver because you need to turn a screw.

That’s why the quality of the tool metadata matters enormously. If get_gateway_logs is described vaguely as “gets logs,” the LLM may not realise it’s the right tool for checking a retried payment and will either pick the wrong tool or give up. The engineer’s skill is partly in writing tool metadata so clear that the LLM reliably reaches for the right one.

Two consequences follow, and both matter:

  • The LLM is only as capable as the toolbox you give it. It cannot check a refund status if you never built and registered a get_refund_status tool. The agent cannot perform external actions beyond the toolbox it has been given, although it can still generate plans, analyses, and reasoning using the model itself.
  • The LLM can still choose wrongly. Because the choice is a judgement based on descriptions, the same non-determinism that makes agents flexible also means they can pick the wrong tool, in the wrong order, or call one they didn’t need the unpredictability we’ll cost out shortly.

4.4 Watching a single agent run

Let us now take a genuinely open-ended task and do a walkthrough on how the agent will process it.

Query :  A customer says they were double-charged last month — investigate and resolve it.

Notice we cannot pre-write the steps. The cause could be a billing bug, a duplicate subscription, a refund that never processed, or a customer misunderstanding. The path depends on what the agent finds. This is why it’s a legitimate agent candidate. Here’s the mechanism and watch how, at each reasoning step, the LLM scans that pre-defined toolbox and chooses:

  1. User goal received → “Investigate the double-charge for customer #4471.”
  2. Reasoning (LLM call #1) → “I don’t know the cause yet. First I should see what this customer was actually charged.” It scans the toolbox, matches that need to the description of get_billing_history, and selects that tool.
  3. Tool call → get_billing_history(customer_id=4471) , the underlying database query runs and returns two charges on the same date.
  4. Observation & state update → the result is recorded: “Confirmed: two charges, $2,000 each, 12 July.”
  5. Reasoning (LLM call #2) → “Two charges confirmed. Now I need to know whether one was a failed-then-retried payment or a genuine duplicate.” It scans the toolbox again, matches this to get_gateway_logs, and selects it.
  6. Tool call → get_gateway_logs(...) → the API call returns: one payment succeeded, one was a retry that also succeeded (a bug).
  7. Reasoning (LLM call #3) → “This is a genuine duplicate caused by a retry bug. The right action is to refund one charge.” It selects issue_refund.
  8. Tool call → issue_refund(...) → the API call moves real money; refund succeeds.
  9. Reasoning (LLM call #4) → “Refund done. Now I should tell the customer.” It selects send_email and generates the message.
  10. Termination → the agent decides the goal is met and stops.

Four separate LLM “reasoning” calls, and each one did the same thing: read the current state, scan the toolbox, and decide which tool fits next. The intelligence isn’t in the tools. The intelligence is in the LLM matching its intent to the right tool, one step at a time. But how does it “read the current state” if the model forgets everything between calls? That’s the machinery we look at next.

4.5 What the agent actually “remembers” — context, memory, and state

It’s tempting to imagine the agent as a colleague who remembers what they just did. It isn’t. Here’s the fact that surprises almost everyone:

The underlying LLM is stateless between API calls. In plain English, this means the model has no memory of previous interactions. Every time it is asked to do something, it starts with a blank mind. Any apparent memory comes from external systems that retrieve and re-insert information into the model’s context.

So how does the billing agent “recall” that it already found two charges? Because the harness re-feeds it the entire history on every single call. The intelligence has no memory; the scaffolding around it does. Let’s name the pieces:

  • Context (the context window) — the LLM’s entire field of vision in every call: the system instructions, the descriptions of every tool in the toolbox, the original goal, and the running transcript of everything done so far. Because the model is stateless, anything you want it to “know” must be placed into the context on every call.
  • Working memory (short-term) — the running scratchpad for this task: each thought, action, and observation, accumulated as the loop runs. In a simple agent, the working memory is the growing transcript that gets stuffed back into the context each loop. It exists only for the life of the task, then vanishes.
  • Long-term memory — information kept beyond this task, in an external store (a database, a vector store, a file) — e.g. “customer #4471 raised a similar issue in March.” It is not automatically in context; the agent must deliberately retrieve it (via a tool) and inject it, and write to it if it wants to remember something for next time.
  • State — the harness’s structured record of where we are: which steps are done, the latest tool outputs, and whether the goal is met. The state is what the loop checks to decide “reason again” or “stop.”

The key dynamic: after every reasoning step and every tool call, the harness updates the working memory and state by appending the newest thought, action, or observation and then the next reasoning call re-sends the whole enlarged context. Long-term memory only changes if the agent explicitly writes to it.

Watch it happen across our billing example. Notice how the context grows at every step — this is the engine of the token bill:

StepWhat the LLM is sent (context window, cumulative)Working memory / state after the stepLong-term memory
StartSystem prompt + all tool descriptions + goal(empty)(could retrieve: “#4471 — no prior disputes”)
Reason #1…the aboveThought: pull billing history Action: get_billing_history(4471)
Tool #1(no LLM call — tool runs)Obs: 2 charges, 2,000, 12 Jul
Reason #2System + tools + goal + Thought1 + Action1 + Obs1Thought: check for retry Action: get_gateway_logs(...)
Tool #2(tool runs)Obs: one payment retried — bug
Reason #3all of the above + Thought2 + Action2 + Obs2Thought: genuine duplicate → refundAction: issue_refund(...)
Tool #3(tool runs)Obs: refund success
Reason #4everything above + Obs3Thought: notify customer → drafts email → terminateWrites: “Resolved double-charge for #4471; cause = retry bug”

Read the middle column top-to-bottom: the context the model must process grows on every loop. By Reason #4, the LLM is re-reading the goal, the full toolbox descriptions, and every thought, action and observation from the entire run, just to decide one final step.

This is the counter-intuitive cost engine of every agent. The model’s “memory” is really the harness re-reading the whole transcript aloud before each decision. In naive agent implementations, token costs compound because the full history is repeatedly reprocessed. More advanced systems reduce this through summarisation, pruning, retrieval, and memory compression. And it’s why context management (trimming, summarising, offloading to long-term memory) is one of the hardest parts of building a reliable agent, a cost that never appears in the demo, only in the bill.

4.6 From one agent to many — what a multi-agent system actually is

A multi-agent system is a team of AI agents, where each agent is responsible for a specific task. They communicate with one another, and a manager agent coordinates their work and combines their results to solve a larger problem.

It is not one brain with many skills. It is many separate reasoning loops each incurring its own LLM calls, each re-sending its own growing context (the exact engine we just saw in 4.5), and now also spending calls on talking to each other. Every metaphorical “meeting between colleagues” is, under the hood, more LLM calls and more tokens.

The typical parts:

  • Orchestrator / Manager agent — receives the overall goal, breaks it into sub-tasks, delegates each to a worker, and assembles the results. It coordinates; it doesn’t do the detailed work.
  • Worker / Specialist agents — each has a narrow job and its own toolbox.
  • Delegation — the manager deciding which worker gets which sub-task (itself an LLM reasoning step).
  • Inter-agent communication — workers reporting back, and sometimes talking to each other. Every conversation between agents comes at a cost. One agent creates the message, and another must read and interpret it, consuming tokens at both ends.
  • Shared vs. isolated memory — do the agents share one memory, or does each keep its own and pass summaries? This choice massively affects both cost and reliability.

Each “obvious” multi-agent example is defeated by a different disguise, until we reach one that genuinely qualifies.

4.6.1 First disguise: “It’s parallel, so it’s multi-agent”

Here’s the trap most “multi-agent” demos fall into. Suppose the task is: “Research our three rivals — Acme, Globex, and Initech.” The obvious design spins up three research agents, one per company. It looks impressive. But ask the sceptical question a literate leader should ask:

“Couldn’t a single agent just research all three, one after another, in a loop? The output would be identical.”

It absolutely could. So what does splitting into three agents actually buy? Only two things and, crucially, not the thing that justifies a true multi-agent system:

  • Parallelism → lower latency. Three workers run simultaneously, cutting wall-clock time to roughly a third. But the trap, it is not cheaper. You still pay for every call and token. Parallelism buys speed, never cost.
  • Context isolation → reliability. A single agent doing all three sequentially would pile all three companies’ research into one ballooning context (per 4.5), risking degraded attention and facts about Acme bleeding into the Globex summary. Three isolated workers each keep a clean, focused context.
  • Specialisation none. The three tasks are identical: same skill, same toolbox, three times over.

Verdict: three-identical-companies is not a true multi-agent case it’s a “parallel fan-out” pattern, justified only by latency and context hygiene. A single looping agent is a perfectly respectable alternative. Showcasing it as “multi-agent” is quietly overselling the pattern.

The disguise it wore: parallelism. We saw through it. But there’s a second, subtler disguise.

4.6.2 Second disguise: “It’s specialised, so it’s multi-agent”

Let’s fix the flaw in 4.6.1 by choosing a task that is genuinely specialised. Take acquisition due-diligence“Assess TargetCo as an acquisition.” This clearly splits into three experts, each with a different toolbox and different domain reasoning:

WorkerIts toolbox (genuinely different)Its expertise
Financial analystfinancial-data API, calculatorvaluation, ratios, cash-flow health
Legal & risk analystlitigation database, contract searchlawsuits, liabilities, compliance flags
Market analystweb search, news APImarket position, competitive threats

This feels like the textbook multi-agent case real division of labour, three distinct specialties. Surely now we need agents?

No. And this is the most important lesson in the whole commandment 2. Watch what happens when we try to break the task with plain, fixed LLM calls:

StepActionTypeWhat produces it
1Pull financial data for the targetTool callcall_financial_api(target)
2Summarise financial healthLLM callfed the data from step 1
3Pull litigation recordsTool callquery_litigation_db(target)
4Summarise legal riskLLM callfed the cases from step 3
5Search market & newsTool callweb_search(target)
6Summarise market positionLLM callfed the news from step 5
7Combine into a final assessmentLLM callfed all three summaries above

Four LLM calls, three tool calls, in a fixed sequence I drew in advance. No agent decided anything. This is a specialised parallel workflow not an agentic system at all.

So where did the intuition go wrong? It made the single most common error in this entire field:

It confused specialisation with agency. They are not the same thing.

  • Specialisation (different toolboxes, prompts, expertise) justifies different branches ,which a workflow gives you for free. You do not need agents to have specialised steps.
  • Agency is justified only by a dynamic, discovery driven path where the next step genuinely cannot be known until the previous step reveals what it found.

Due-diligence is a checklist. You know in advance that you’ll pull financials, litigation, and market data so the path is pre-drawable, so it’s a workflow. Specialisation was a real property, but it was never the property that matters.

When would due-diligence actually become agentic? Only if it must branch on its findings: the financial analyst spots an odd related-party transaction → decides on its own to pull a subsidiary’s filings → forms a hypothesis of inflated revenue → chases three more documents to test it → follows the trail into an entity nobody put on the checklist. That path can’t be pre-drawn. But the ordinary “produce a due-diligence report” task never needs it. Most corporate due-diligence is checklist-driven i.e., a workflow.

Verdict: due-diligence is a specialised workflow, not a multi-agent system. It cleared the specialisation bar but failed the one that counts — agency.

The disguise it wore: specialisation. We saw through that too. So what does a genuine multi-agent system look like?

4.6.3 The real thing: parallel + specialised + discovery-driven

To genuinely need a team of agents, a task must clear all three hurdles at once: it must be parallel (not just fan-out), specialised (not repetition), and agentic (a discovery-driven path in each strand). Here is a task that finally does and it builds on an idea any leader will recognise: an analytics agent that builds answers on the fly.

The single-agent version first. Consider one exploratory question: 

“Why did our Q3 margins drop in APAC?” 

An analytics agent with tools (run_querytransform_datamake_chart) would:

  1. Decide which data to pull → write a query → run it → see “margins fell 4pts,”
  2. Based on that, drill into product mix → write a new query → see “one product line drove it,”
  3. Based on that, drill into a specific SKU → discover a cost spike,
  4. Decide a waterfall chart best communicates the finding → generate it.

Can you pre-draw this flowchart? No. Query 2 depends on the result of query 1; the chart type depends on the finding; the number of drill-downs isn’t known until the data reveals where the anomaly hides. The path is discovered, not designed. This is a genuine agent but for one question, it is a single agent (the steps are sequential and dependent; there’s nothing to parallelise).

Now scale it into a true multi-agent system. Change the request to an open-ended, multi-domain one:

“Give me a full quarterly business review, what’s happening across Sales, Operations, Finance, and Supply Chain?”

Here’s the important nuance, because it’s where magic-thinking creeps in: the analyst agents and their toolboxes are defined upfront. Someone engineered a Sales analyst wired to the sales DB and CRM, a Finance analyst wired to the GL, and so on. You decide your domains and build their tools in advance. What the lead agent does dynamically is decide, for this query, which of those analysts to invoke, write each a specific brief, and in more advanced setups decide how many parallel investigations to spawn within a domain (say, three sales sub-investigations for three anomalous regions it wants explored at once).

So “spinning up agents on the fly” is real, but bounded: the lead agent varies the orchestration, who works on what, and how many within a fixed, pre-engineered envelope of tools and data access. It never invents a new capability. Ask it a question in a domain you never built tools for, and it is simply blind to it. The intelligence is dynamic; the capability envelope is not.

Analyst agentIts toolsWhy it’s genuinely agentic
Sales analystsales DB, CRM query, chartingexplores revenue, follows leads into whichever regions/segments look anomalous
Operations analystops-metrics DB, chartinginvestigates throughput/quality, drills wherever the data looks off
Finance analystledger/GL API, chartingexplores margins/cash, chases the drivers it discovers
Supply-chain analystlogistics DB, supplier APIinvestigates delays/costs, digs where the trail leads

(These four analysts and their tools are defined by your engineering team upfront; the lead agent chooses which to deploy and how many parallel instances per query)

Now every box is genuinely ticked:

  • Parallel — the four domains are independent and don’t need to talk mid-flight.
  • Specialised — different data sources, different domain reasoning.
  • Agentic — each analyst runs a real, unbounded discovery loop; none of the four paths can be pre-drawn. (This is the exact property due-diligence lacked.)
  • Too large for one context — four deep explorations won’t fit cleanly in one agent’s memory without context rot.
  • Coordination worth it — parallelism gives a materially faster review; isolation keeps each investigation clean.

The decisive contrast with 4.6.2: in due-diligence, each worker ran a fixed two-step chain (pull → summarise) workflow. Here, each analyst runs a genuine, openended discovery loop (agent). 

Four parallel agents doing genuine discovery = a real multi-agent system.

The other classic clean example, if you prefer a non-analytics one: agentic deep research — a lead agent spawns parallel researcher sub-agents, each doing open-ended web investigation that revises its own search strategy based on what it finds. Same fingerprints: parallel + specialised + discovery-driven.

4.6.4 How the machinery runs : the blackboard flow

The critical structural difference from a single agent is that there is no shared brain. Each analyst has its own isolated context and working memory, and can only learn about the others through messages passed via the lead agent. The lead agent holds the one global state / shared memory (often called a blackboard). Here’s the flow, tracking every piece of machinery, for our business review:

#EventLead agent’s blackboard (shared state)What each analyst holds (isolated)
1Goal receivedGoal: quarterly review; strands:[sales,ops,fin,supply]; dispatched:0; returned:0}
2Lead reasons → plans the four investigations (1 LLM call)plan written to blackboard
3Delegation → lead generates 4 briefs, sends as messagesdispatched:4, returned:0Each analyst’s context is created: only its own brief + its own tools
4Sales analyst runs its own discovery loop(query→observe→drill→chart→…)lead waits — no visibility inside the analystSales memory: only sales queries/findings
5Ops analyst runs its own discovery loop(parallel)no visibilityOps memory: only ops findings – never sees sales context
6Finance & Supply analysts run their own loops (parallel)no visibilityEach: only its own domain findings
7Analysts report back → each generates a summary + charts messagelead appends all 4 reports → returned:4analyst contexts now discarded
8Lead reasons over the combined blackboard (now a large context)synthesises the 4 strands into one review
9Terminate → returns the assembled business reviewfinal result on blackboard

Three dynamics you should take away:

  1. Memory is isolated, not shared. The sales analyst literally cannot see what the finance analyst found. This is a feature (clean, focused contexts) and a liability (the lead agent is the only place the full picture exists a single point of failure, and the place where one analyst’s hallucinated number gets baked into the final review as fact).
  2. Every arrow between agents is tokens. Each brief (step 3) and each report (step 7) is text, one agent generates and another reads, pure coordination overhead producing no direct output. And it sits on top of each analyst’s own discovery loop, which carries the very same growing-context cost engine from 4.5, now running four times in parallel, plus the lead’s.
  3. The lead’s synthesis context is huge. Step 8 pulls all four full reports into one context often the single most expensive call in the entire run.

4.6.5 The three-strike summary

We defeated two disguises before finding the real thing:

ExampleLooks like…Actually is…Fails on…
4.6.1 Three identical companiesmulti-agentparallel fan-out (a single looping agent works)no specialisation
4.6.2  Due-diligence reportmulti-agentspecialised workflow (fixed, pre-drawable path)no agency
4.6.3  Multi-domain business reviewmulti-agentgenuinely multi-agent nothing; it passes

The lesson compounds beautifully: to justify a multiagent system, “it’s parallel” isn’t enough and “it’s specialised” isn’t enough. You need the one property that actually matters everywhere in this commandment — an unpredictable, discovery-driven path happening in parallel, specialised strands. That is a genuinely high bar, which is exactly why real multi-agent systems are far rarer than the word’s popularity suggests.

5.0 The experiment: what over-engineering actually costs

Now you might be thinking:

“Fine. You’ve shown me three architecture’s — a workflow, a single agent, a multi-agent system. Big deal. What if I just run multi-agent for everything? It sounds sophisticated. It’ll make us look cutting-edge. Where’s the harm?”

Wait a minute. It’s not about looking cool or cutting-edge. There is always a cost — and it is a perpetual one. This is the crucial word most people miss. Building the fancy system is a one-time cost you can rationalise away. But an agent doesn’t cost you once; it costs you on every single transaction, forever. Every email, every query, every invoice — for the entire life of the system.

That perpetual cost comes in two forms:

  1. Token generation cost — the actual dollars you pay the model provider for every input and output token, on every run.
  2. Time (latency) — how long each transaction takes, which becomes its own cost when a customer is waiting or a queue is backing up.

So rather than argue about it, we ran a real experiment. Here’s what we found.

5.1 The experiment: one simple task, three designs

We took a deliberately simple, deterministic, predictable task, the kind that occurs millions of times in real businesses:

Inbound customer-email triage:

Receive email → Classify intent (billing/technical/sales) → Look up the order → Draft a reply → Route to the right queue

The path never changes. Every email follows the same steps in the same order. We know the road which, per this whole commandment, means a fixed workflow is the correct design. But to measure the cost of over-engineering, we built it three ways and compared the results:

  • Design A — Workflow (correct): three fixed LLM calls : classify → extract → draft. I decide the sequence; the model just fills in each blank.
  • Design B — Single agent (over-engineered): one ReAct agent given the goal “triage this email” plus tools, left to discover the sequence itself on every email.
  • Design C — Multi-agent (absurdly over-engineered): a manager agent delegating to a classifier, an investigator (with its own tool-using loop), and a drafter all sharing one Gemini model, passing messages back and forth.

5.2 The results (real runs, Gemini 2.5 Flash, averaged with spread)

Before reading the numbers, here’s exactly what was measured — so you can trust the table rather than take it on faith.

  • One fixed task, three architectures. Every design was handed the identical input — a single customer support email (“I think I was charged twice for order #A-5592”) and asked to produce the same outcome: a classified, investigated, drafted, and routed reply. The only thing that changed between runs was the architecture (fixed workflow → single agent → multi-agent), never the task. This is what makes it a fair, apples-to-apples comparison: any difference in cost is caused purely by how we built it, not what we asked.
  • Real calls to a real model. These are not simulated numbers. Each design made live calls to Gemini 2.5 Flash via Vertex AI, using LangChain/LangGraph to orchestrate the single-agent and multi-agent versions. The workflow used direct, fixed model calls.
  • Measured directly from the model’s own metering. For every LLM call we captured the input tokens and output tokens straight from the model’s usage_metadata (not estimated), counted the number of LLM calls, and timed the wall-clock latency end-to-end. Cost is computed from those real token counts.
  • Repeated runs, with the spread reported. Crucially, we didn’t run each design once and quote a lucky number. Each architecture was run multiple times, and we report the mean ± standard deviation for every metric. That ± is deliberate: it lets us see not just how much each design costs, but how predictable that cost is run-to-run which, as you’ll see, turns out to be one of the most revealing findings of all.

With that established, here are the results.

DesignLLM callsInput tokensOutput tokensTime (s)Cost ($)× vs. workflow
A. Workflow (fixed)3 ± 0228 ± 095 ± 06.6 ± 0.30.0001 ± 0.00001.0×
B. Single agent3 ± 0929 ± 0240 ± 05.1 ± 0.40.0003 ± 0.00003.1×
C. Multi-agent6 ± 0866 ± 21802 ± 12320.3 ± 0.70.0006 ± 0.00016.7×

(Cost $ above uses the script’s default illustrative pricing . We recompute with real Gemini 2.5 Flash rates later )

The ± values come from running each design multiple times. These values carry one of the most important findings in this whole post. Let us look at the insights hiding in the table

1. Same task. Same output. Wildly different cost. All three designs produced the same triaged email. Yet moving from workflow to multi-agent multiplied the token bill several times over. You paid more a lot more for identical business value.

2. The single agent made the same number of calls (3) but burned ~4× the input tokens. This is the subtle one. The agent didn’t take extra steps here; the task was simple enough to solve in three reasoning turns. So why 929 input tokens versus the workflow’s 228? Because every one of the agent’s turns re-sends the growing context plus the full tool descriptions the “stateless model re-read the whole transcript” from Section 3.5. Even when an agent takes no extra steps, the harness itself is a drain. You pay 4× just for the scaffolding, before the agent does anything clever.

3. The multi-agent output tokens exploded — 802 vs. 95, more than 8×. This is the message-passing “tax” made visible. Each agent generates a verbose brief or report that another agent then reads. All that inter-agent chatter is output token’s pure coordination overhead that produced zero additional business value.

4. Predictability collapses as you move right — look at the ± column. The workflow is perfectly deterministic: ± 0 on everything. The single agent: still ± 0 on tokens (it happened to find the same path each run). But the multi-agent swung by ± 123 output tokens run-to-run. When you hand the LLM the steering wheel, you don’t just lose money and speed you lose predictability itself, the one thing a production process needs most. A cost you can’t forecast is a cost you can’t budget.

5. The latency story is honest and still damning. Notice the single agent (5.1s) was actually slightly faster than the workflow (6.6s). At this tiny scale, the difference between A and B is within network noise don’t over-read it. But the multi-agent at 20.3s is ~3–4× slower, unambiguously, and outside all noise. Every hand-off is another sequential round-trip. When a customer is waiting on a reply, that 20 seconds is a real, felt cost.

5.3 Now the real money: cost per email, at real Gemini prices

The table above used the script’s placeholder prices. Let’s use the real ones. Gemini 2.5 Flash charges $0.30 per million input tokens and $2.50 per million output tokens note that output is 8.3× more expensive than input, which matters enormously in a moment.

Recomputing each design’s cost for one single email:

DesignInput tokensOutput tokensInput $Output $Cost / email×
A. Workflow22895$0.0000684$0.0002375$0.0003061.0
B. Single agent929240$0.0002787$0.0006000$0.0008792.9
C. Multi-agent866802$0.0002598$0.0020050$0.0022657.4

Here’s a beautiful, sobering detail: under real Gemini pricing the multi-agent penalty gets worse 7.4×, not 6.7×. Why? Because output tokens cost 8.3× more than input, and the multi-agent design is output-heavy (all that inter-agent chatter). The cost multiplier isn’t fixed it depends on your model’s input/output price ratio, and the fashionable architecture is precisely the one that leans on the expensive side of that ratio. Choose a model with pricier output, and over-engineering punishes you even harder.

Fractions of a cent per email sound trivial. That’s the trap. Watch what “perpetual” does to a trivial number.

5.4 Scale it up: the perpetual token guzzler

These are not one-time costs. They fire on every message, every month, forever.

And here’s the key reframe before we scale: email triage is just our illustrative example. The classify → investigate → draft → route pattern is completely channel-agnostic the identical architecture runs on every inbound support contact, whether it arrives as an email, a live-chat message, a web ticket, or an in-app query. So when we scale up, we’re not imagining a company drowning in ten million emails; we’re counting the total inbound support messages a large consumer business genuinely handles across all its channels a number that comfortably reaches into the millions per month.

Scaling the real per-contact figures to realistic business volumes:

Monthly volume (all channels)A. WorkflowB. Single agentC. Multi-agent
100,000 contacts/mo$31$88$226
1 million contacts/mo$306$879$2,265
10 million contacts/mo$3,059$8,787$22,648

And annually, at large-consumer scale (10M/month):

A. WorkflowB. Single agentC. Multi-agent
Per year$36,708$105,444$271,776

Look at that final row. For a task where the workflow was the correct design, choosing “multi-agent because it’s cool” costs you an extra ~$235,000 every single year, forever to produce the exact same triaged replies. That is not an investment. That is a perpetual token guzzler bolted onto your P&L, quietly draining margin on every transaction until someone finally asks why the inference bill is so high.

This is the difference between a build cost and a run cost. You can absorb a bad build once. A bad architecture bleeds you every day it runs.

And support triage is just one process. Most enterprises run dozens of these high-volume, repetitive, perfectly-predictable AI tasks — document classification, invoice extraction, content moderation, KYC checks, log triage, review summarisation. Each one is a candidate to be quietly over-engineered into an agent, and the 7.4× penalty compounds across every one you get wrong. The quarter-million-dollar line above isn’t a ceiling reserved for tech giants it’s the cost of a single mis-architected process. Multiply it by the dozen such processes a typical operation runs, and “let’s just use agents for everything” stops looking cool and starts looking like a structural margin leak. And yet, for the first two years of this wave, high token consumption wasn’t treated as a leak at all it was treated as a trophy. The industry had a name for it: token maxing.

5.5 The rise and fall of “token maxing”

Token maxing was the mindset that more tokens consumed was itself the mark of a sophisticated AI operation as if there were an industry leaderboard for who could burn the most. Longer reasoning traces, fatter contexts, more tool calls, more agents: consumption became a proxy for capability, and teams effectively competed on it. What the leaderboard conveniently ignored were the only two things that actually matter the cost of all those tokens, and whether they produced any better outcome.

Then the invoices arrived.

Companies that industrialised agentic systems without discipline discovered that token consumption doesn’t scale linearly with value it scales with architecture. A team could 10× its inference bill and see no improvement in business outcomes, because it had bought reasoning overhead, not results. The very “sophistication” they were proud of was the thing draining the budget.

In 2026, Uber reportedly burned through its entire annual AI budget within the first quarter exhausting a full year’s allocation by roughly March. Not a build overrun. Not a one-off migration. A run-rate so far above forecast that twelve months of budget evaporated in three because that’s what perpetual, per-transaction token consumption does when it’s left unconstrained. The invoice doesn’t arrive once; it arrives on every call, every day, and it compounds silently until the annual number is gone.

Uber isn’t alone, and it isn’t incompetent it’s an early, visible example of a pattern now playing out across the industry: teams that optimised for capability (“more agents, more reasoning, more tokens”) without a matching discipline on cost-per-outcome discovered that the two are not the same thing. The reasoning overhead they were proud of was the line item quietly draining the budget.

The lesson isn’t “don’t use AI.” It’s the one this entire experiment has been building toward: the flashy architecture and the frugal one often produce the identical result, but only one of them empties your budget by March

The correction is now well underway. Mature teams have swung hard toward token prudence: default to the simplest architecture that works, reserve agents for genuinely discovery-driven paths (Section 3), cache aggressively, trim context, use smaller models where they suffice (a preview of Commandment 9), and above all measure cost-per-successful-outcome, not cost-per-clever-demo. The status symbol is no longer “we run a multi-agent swarm.” It’s “we deliver the outcome at a fraction of the token cost.”

Our little experiment is that lesson in miniature: the cheapest design (A) and the flashiest design (C) produced the identical result. One of them cost 7.4× more, forever.

5.6 And the token bill isn’t even the whole bill

Everything above is just tokens and time. There’s a second, harder-to-price cost that compounds the first: reliability.

The cost compounds, it doesn’t add. With multi-agent you pay for every worker’s own reasoning loop (each re-sending its growing context and tool descriptions), plus the manager’s coordination calls, plus the tokens agents spend talking to each other. Three multiplying sources, not three additive ones.

And the failure modes multiply too. With a fixed workflow, if something breaks, you know exactly which of the three steps failed. With a single agent, a wrong tool choice is one bug in one loop. But with a team of agents:

  • a manager can mis-delegate — send the wrong sub-task to the wrong worker;
  • a worker can misunderstand its brief — it only has the summary the manager chose to pass, not the full context;
  • errors propagate — one worker’s hallucinated “fact” gets synthesised into the final output as truth;
  • and when the output is wrong, you must debug which agent, in which loop, on which call went astray dramatically harder than tracing a single agent, and worlds away from a fixed workflow where the path is nailed down.

That debugging difficulty, that unpredictability (remember the ± 123), and that wider blast radius are all real operational costs that never appear on the model provider’s invoice but land squarely on your engineering team, every week.

The rule that follows: every additional agent multiplies cost and fragility. A multi-agent system must therefore clear a higher bar than a single agent not a lower one. Splitting a task across four agents does not make it four times smarter. It makes it several times more expensive, several times slower, and several times harder to trust.

5.7 The takeaway

We ran the experiment so you don’t have to learn this from your own invoice. On a task that genuinely needed a workflow:

  • The workflow and the multi-agent system produced identical output.
  • The multi-agent version cost 7.4× more per transaction a gap that only widens with pricier output models.
  • It ran ~4× slower, was less predictable run-to-run, and was far harder to debug.
  • And every one of those penalties is perpetual, paid on every transaction, for the life of the system.

So when someone says “let’s just run multi-agent for everything, it’ll make us look cool” this is the number to put on the table. Cool is a one-time feeling. The token bill is forever.

Match the architecture to the road. Where the road is known, build the workflow. Reserve agents for the roads that genuinely have no map and reserve multi-agent for the rare roads that fork into parallel, specialised, genuinely-unmapped territory at once. Everywhere else, do not take the agent’s name in vain.

6.0 When you genuinely DO need an agent

The experiment might read like an anti-agent polemic. It isn’t. Agents are not the villain — misapplied agents are. The whole point of paying the agent tax is that, for the right problem, it buys you something a workflow simply cannot: the ability to navigate a road that has no map.

Reach for an agent only when the problem shows these fingerprints:

  • The path is unknown in advance. You genuinely cannot pre-draw the flowchart, because the next step depends on what the previous step discovers. (Our billing-dispute investigation in 4.4: yes. The email triage we just benchmarked: no.)
  • It’s genuinely multi-step and adaptive. The task requires forming a hypothesis, testing it, and changing course based on the result.
  • It requires dynamic tool use. Which tools, and in what order, depends on the situation as it unfolds.
  • Uncertainty is inherent, and exploration has value. The messy, branching nature of the problem is the whole point where an agent would add value

7. The decision lens: agent or not?

Put your process step usually the constraint you identified in Commandment 1 through these questions:

Ask thisIf YES →If NO →
Can I draw the exact steps in advance?WorkflowConsider agent
Does every run follow the same path?WorkflowConsider agent
Does the next step depend on what the last step discovered?Consider agentWorkflow
Does it need to adapt, form hypotheses, and change course?Consider agentWorkflow
Is unpredictability acceptable here?Agent viableWorkflow / human
Would a wrong autonomous action be costly or irreversible?Add human checkpointsAgent viable

Rule of thumb: If you can draw the flowchart, build the flowchart. Only when you genuinely can’t draw it, does an agent earn its existence.

7.1 The second lens: single agent or multi-agent?

The first lens gets you to “yes, this genuinely needs an agent.” But that’s only half the decision. The fashionable and expensive mistake is to leap straight from “we need an agent” to “let’s build a multi-agent system.” As our experiment showed, that leap can multiply your perpetual cost several times over for no added value.

So once the first lens says “agent,” run the candidate through this second lens before allowing more than one:

Ask thisIf YES If NO 
Do the sub-tasks run genuinely in parallel (independent, no need to talk mid-flight)?Consider multi-agentSingle agent
Do they need genuinely different toolboxes or expertise (not the same skill repeated)?Consider multi-agentSingle agent
Is each strand itself discovery-driven (an unmapped path, not a fixed pull-and-summarise)?Consider multi-agentSingle agent (it’s a workflow of specialists)
Is the combined work too large for one agent’s context to hold cleanly?Consider multi-agentSingle agent
Does the value of parallelism/isolation exceed the extra tokens, coordination, and debugging pain?Multi-agent viableSingle agent

You need all five to point toward multi-agent. Miss even one, and a single agent is the right and far cheaper answer. This is deliberately a high bar, because the default gravity of the industry pulls the other way.

8. The business leader’s checklist

The next time someone says “let’s put an agent on it,” ask these before nodding:

  • ☐ “Can you draw me the flowchart of steps?” — If they can, you want a workflow, not an agent.
  • ☐ “Does the path change based on what the process discovers along the way?” — If not, an agent is overkill.
  • ☐ “How many tools does it need and who builds and maintains each one?” — Every tool is a real, recurring integration. Tool count is a proxy for true cost and fragility (Section 4.1).
  • ☐ “What will this cost per transaction, at our volume, versus a fixed workflow?” — Make them run the numbers. Remember these are perpetual costs (Section 5).
  • ☐ “How much slower will each transaction be?” — Latency is a real cost when a customer or a line is waiting.
  • ☐ “When it misbehaves, how will we know why and which step or agent failed?” — Probe their debugging and observability story.
  • ☐ “What can this agent do in the real world, and what happens if it does the wrong thing?” — Establish the blast radius and the human checkpoints (previewing Commandment 8).
  • ☐ “Why not a single prompt or a simple chain first?” — Put the burden of proof on the more complex option, not the simpler one.
  • ☐ “If it’s multi-agent can you tick all four boxes (parallel, specialised, too-large-for-one-context, coordination-worth-it), or is this org-chart cosplay?” — Force a hard justification for every agent beyond the first.

If the honest answers point to a known, repeatable path, you don’t have an agent problem. You have a workflow which, conveniently, is the subject of the next commandment.

9.0 Conclusion

An agent is not a magic upgrade. It is the same LLM you already use, wrapped in a harness that lets it choose its own path and act on its own and you pay for that autonomy in tokens, time, unpredictability, and operational pain. We didn’t assert this; we measured it: on a task that genuinely needed a workflow, the multi-agent version produced identical output at 7.4× the perpetual cost, ran ~4× slower, and was far harder to trust.

That price is worth it when the road is genuinely unknown and the task must adapt as it goes. It is wasted as our experiment showed, up to ~7× over, forever when the road is known, which describes the majority of business processes.

The literate leader doesn’t ask “can we make this an agent?” They ask “can I draw the flowchart?”  and reserve the steering wheel for the roads that genuinely have no map.

Invoke agents where they belong. Everywhere else, do not take their name in vain.

Next up — Commandment 3: “Honour the Humble LLM Workflow.”

If most processes don’t need an agent, what do they need? We’ll make the case for the unglamorous, reliable, cheap workhorse of business AI — the single well-crafted prompt and the fixed chain — and show how far a “boring” workflow can actually take you.

Commandment 1 – “The Process Is Sovereign; Thou shall not put Technology Before It”

Part 2 of the series The 10 Commandments of AI in Business: Choosing the Right Intelligence for the Right Problem

Recap

In the introduction to this series, we made a single argument: that roughly 95% of enterprise generative-AI pilots fail (MIT, 2025) not because the technology is weak, but because of a failure of judgement — force-fitting the fashionable tool onto the wrong problem. AI literacy, we said, is knowing which intelligence to reach for, and when.

We laid out ten commandments to build that judgement. In one line each:

  1. The process is sovereign — start with the process, not the technology.
  2. Don’t invoke agents in vain — most problems don’t need one.
  3. Honour the humble LLM workflow — a single well-crafted call often wins.
  4. Honour predictive AI and automation — the proven elders solve most problems.
  5. Don’t slay your token budget — ground and retrieve before you build big.
  6. Be faithful to causation — correlation misleads when you’re deciding to act.
  7. Don’t steal the work that belongs to another tool — compose the right blend.
  8. Don’t let your agent bear false witness — autonomy demands accountability.
  9. Don’t covet only the frontier models — smaller, specialised models often fit better.
  10. Don’t covet your competitor’s AI — prove the cost-benefit in your context first.

If those ten are the map, this post is where we take the very first step — and it is the step every other commandment stands on. Because before you can decide whichAI to use, you have to understand the thing you’re actually trying to improve: your business process.

1.0 Introduction

Let us first replay a scene probably you have lived through

A problem is raised in a meeting :-

  • Quotes are going out too slowly,
  • Defects are slipping through,
  • Campaigns are late.

Before anyone has looked at where the delay actually lives, someone says the sentence you now hear in every conference room:

“Why don’t we just put an AI agent on it?”

Heads nod. A pilot is funded. Six months later it joins the roughly 95% of enterprise generative-AI pilots that, per MIT’s 2025 study, delivered no measurable business impact.

The problem was never that the AI model was inadequate. It was never the prompt. It was never the framework. The mistake happened within the first few minutes of the meeting. The team asked, “Where can we apply AI?” when they should have asked, “Where does our business process actually need help?”

The rule is simple, you do not start with “Where can I use AI?” You start with “What is my process, where does it bleed, and what is the cheapest, most reliable thing – generative AI, predictive AI, automation, or a manual effort that stops the bleeding there?” That distinction is the foundation of this entire series. Everything else : the choice of LLM, predictive model, workflow, agent, or automation is secondary.

2.0 The business process is sovereign. Technology exists only to serve it.

Imagine three companies.

  • Company A has access to the world’s most advanced AI models.
  • Company B has average AI capability.
  • Company C barely uses AI.

Which company wins? Most people instinctively choose Company A. The correct answer is:

The company with the best business process.

Technology amplifies process quality. It rarely compensates for poor process design. An AI agent cannot rescue a process that contains unnecessary approvals. An LLM cannot eliminate a policy bottleneck. Automation cannot compensate for poorly designed governance. Predictive AI cannot improve decisions if the wrong data is collected. A badly designed process simply becomes an expensive badly designed process. The first lesson every business leader must learn is therefore remarkably simple.

Never optimise technology before understanding the process it serves.

3.0 The Forgotten Science of Throughput

Long before Generative AI existed, operations researchers had already solved a surprisingly large part of this problem. In 1984, Eliyahu Goldratt introduced the Theory of Constraints in his influential book The Goal. The idea is almost deceptively simple.

Every business process consists of a chain of activities. Like the strength of a chain being limited by its weakest link, the throughput of a process is determined by a single limiting step, the constraint. Improving anything other than that constraint rarely increases the output of the entire system.

Imagine a six-lane highway that narrows into a single toll booth. Would building two more lanes before the toll booth reduce congestion? Of course not. It simply allows more cars to reach the bottleneck faster.

Exactly the same mistake happens in AI projects. Organisations automate whatever is easiest to automate rather than what actually limits business throughput. The result is impressive demonstrations but disappointing business outcomes.

Goldratt’s five focusing steps:

  1. Identify the constraint.
  2. Exploit it – get everything you can out of it as-is.
  3. Subordinate everything else to it.
  4. Elevate it – invest to break the constraint.
  5. Repeat – the constraint will move; go find the new one.

Goldratt’s framework answers the where. The next challenge is answering the how.If the bottleneck has been identified, how should we remove it? Should we redesign the workflow? Introduce automation? Deploy predictive AI? Use Generative AI? Build an autonomous agent? Or simply hire another experienced employee?Before choosing any of these, we must first understand what the process is fundamentally trying to accomplish. Every process exists for one reason – to produce valuable business outcomes.

  • A sales process exists to convert qualified opportunities.
  • A manufacturing process exists to produce good units.
  • A recruitment process exists to hire capable employees.
  • A marketing process exists to launch campaigns.

Every AI investment should therefore answer one question.

How does this improve throughput?

If the answer is unclear, the AI project is probably solving the wrong problem. Of course, this immediately raises another question.

How do we systematically determine whether an AI intervention will improve throughput?

The following five-step discipline provides exactly that. It acts as a decision-making framework that helps identify where AI can genuinely create value, what type of intelligence is most appropriate, and just as importantly, where AI should not be applied at all.

4.0 The Five-Step Discipline Before You Think About AI

Before selecting any technology, every executive should perform five simple exercises.

Step 1 — Define the Business Outcome

Do not begin with technology. Define what valuable output the process exists to produce. Examples include:

  • Good units manufactured
  • Customer claims resolved
  • Campaigns launched
  • Orders fulfilled
  • Policies approved

The metric must describe business value, not activity.

Step 2 — Map the Process

Break the process into its constituent steps.

  • Where is time consumed?
  • Where are decisions made?
  • Where do errors occur?
  • Where does work accumulate?

Without visibility into the process, AI becomes little more than educated guesswork.

Step 3 : Find the Constraint

Now measure – Not opinions, Not assumptions – Actual data.

  • Where is work waiting?
  • Where are queues forming?
  • Where is the largest contributor to cycle time?

That is the only place worthy of serious attention.

Step 4 : Understand the Nature of the Constraint

This is where AI finally enters the discussion. Not all constraints require the same form of intelligence. Ask a simple question.

What kind of work is actually happening here?

Nature of WorkAppropriate Intelligence
Generating content, language or creative ideasGenerative AI
Predicting future outcomes or classifying patternsPredictive AI
Performing repetitive deterministic actionsAutomation
Optimising allocation, routing or schedulingOptimisation algorithms
Coordinating multiple systemsAI Agents
Exercising judgement, empathy or accountabilityHuman expertise

Step 5 — Examine the Ripple Effects

Suppose AI removes today’s bottleneck.

  • What becomes tomorrow’s bottleneck?
  • Have you merely moved the queue downstream?

Every process redesign should consider second-order consequences. Business systems behave as systems, not isolated activities.

To illustrate how the framework supports technology investment decisions, we will evaluate three representative business processes. The metrics used throughout these examples are baseline assumptions designed to explain the core concepts. Because metrics are inherently organization-specific, enterprises must gather their own relevant data to execute this evaluation  effectively.

4.1 Process Analysis 1 – Marketing: multi-channel campaign content production

Let us first take a process from the marketing industry.

Now we will walk through each steps depicted in our analysis framework and apply it on the above marketing process.

Step 1 — Throughput metric. 

This process exists to ship campaign-ready, on-brand assets. Throughput = number of approved campaign variants shipped per week(with a quality floor). Cost-per-asset is the secondary metric.

Step 2 — Decompose. 

Average time per campaign across the steps:

StepAvg. time% of cycle
Brief  -> concept1 day12%
Copywriting (all channels)3 days38%
Visual creation1.5 days19%
Channel variants + localisation2 days25%
Compliance review0.5 day6%
Publish00

Step 3 — Find the constraint empirically. 

Measure where work actually accumulates rather than relying on intuition. In this process, copywriting and content variant production together consume 63% of the total campaign cycle time. Because every campaign must pass through these stages before progressing further, requests begin to accumulate whenever the demand for new content exceeds the team’s capacity to produce it, leaving subsequent stages of the process waiting for content to be completed.

Step 4 — Apply the decision lens across the whole process (not just the constraint)

The instinct at this point is to say “copywriting is the constraint, so drop generative AI on it, done.” That’s half right. Breaking the constraint is priority one ,but a process is a chain, and once you elevate one link, the bottleneck moves. So we walk every step through a richer decision lens, and we do it with one eye permanently on overall throughput.

Let us now do the walk-through. For each step we ask two things: 

  • What is the nature of the work?  and 
  • Does throughput justify investing here? (Theory of Constraints).
  • Receive campaign brief 
Nature of taskDecision LensThroughput view
Capturing and routing an intake request. A structured intake form that logs the brief, tags it, and routes it to the right podAutomationThis sits in the 12% “brief-to-concept” bucket and is not the constraint so keep it cheap and boring. No AI required.
  • Develop creative concept 
Nature of taskDecision LensThroughput view
The strategic “big idea.” This is where two rows collide: it involves generating ideas (Generative AI) and accountability for brand direction (Human expertise). Human-led, GenAI-assisted. The strategist owns the concept; GenAI accelerates ideation, mood-boards, Still inside the non-constraint 12% — use GenAI as a brainstorm partner, not a decision-maker. Don’t over-engineer it.
  • Write copy (headlines, body, CTAs) 
Nature of taskDecision LensThroughput view
Generating language at volume across channels. brand direction (Human expertise). Gen AI  This is the constraint (38%). This is where the primary investment goes and where the 3–5× uplift is earned. Human moves from author to editor.
  • Visual Creation
Nature of taskDecision LensThroughput view
Generating creative assetsGen AI ( image generation) under human art direction  19% and adjacent to the constraint — worth investing, but the human art director stays accountable for brand fidelity.
  • Produce channel variants (email, social, display, landing page) 
Nature of taskDecision LensThroughput view
This is genuinely two kinds of workAdapting tone and message per channel is Generative AI; resizing, reformatting, and fitting each asset to rigid channel specs is a deterministic, repetitive action → AutomationPart of the 25% variant/localisation bucket- a hybrid of GenAI (message) + automation (format) is the right build.
  • Localise & personalise per segment 
Nature of taskDecision LensThroughput view
There are three types of task involved in this stepTranslating and adapting language > Generative AI
Deciding which segment should see which message > Predictive AI (propensity/segment models). 

Deciding how to split budget/impressions across segments > Optimisation algorithms.
Completes the 25% bucket. This single step is the clearest proof that “put AI on it” is a meaningless instruction — three different interventions do three different jobs here.
  • Legal / brand compliance review 
Nature of taskDecision LensThroughput view
Checking content against rules and claims (a classification task) and signing off (an accountability task). Predictive AI for an automated first-pass compliance/claims classifier that flags risky content.
Human expertise for final legal sign-off on the flagged exceptions.
This is the new constraint. Once GenAI multiplies draft volume 5×, all that copy piles up at a 0.5-day manual review. Per Goldratt, you must elevate this immediately. The classifier triages the safe 80% so humans review only the risky 20%. 
  • Schedule & publish 
Nature of taskDecision LensThroughput view
Three tasks within this step Choosing the optimal send time / slot per channel > Optimisation algorithms
Pushing the assets out via platform APIs > Automation
Coordinating a synchronised launch across email, social, display and web systems > AI Agents (this is a legitimate agent use orchestrating multiple systems toward a goal).
Near-zero cycle time today, so don’t gold-plate it but the agent-coordinated publish is where a real agent earns its place (contrast this with the hype reflex of putting an agent on everything).

The throughput-first sequencing rule

We could build all of the above. But Theory of Constraints says, not all at once, and not in random order. Invest in strict order of throughput impact:

  • Wave 1 : Break the constraint: GenAI on copy, visuals, and variants (steps 3–5). This delivers the headline 3–5× gain.
  • Wave 2 : Elevate the new constraint: the compliance classifier (step 7), because that is exactly where the bottleneck jumps once Wave 1 lands.
  • Wave 3 : Smooth the flow: optimisation for scheduling and personalisation, agent-based publishing coordination (steps 6, 8).

Spending on Waves 3 before Wave 2 is the “widen the road before the toll booth” mistake you’d just deliver more copy to a jammed review desk.

The result: right tool, right step, right sequence

#Process stepNature of workAppropriate intelligenceConstraint?Throughput priority
1Receive briefDeterministic intakeAutomationNoLow — keep cheap
2Develop conceptIdea generation + judgementHuman + GenAI assistNoLow
3Write copyGenerating languageGenerative AI (human edits)Yes *Wave 1
4Source visualsGenerating creativeGenerative AI (human art dir.)AdjacentWave 1
5Produce variantsMessage = generate; format = deterministicGenAI + AutomationAdjacentWave 1
6Localise & personaliseLanguage + targeting + allocationGenAI + Predictive AI + OptimisationNoWave 3
7Compliance reviewClassify content + accountable sign-offPredictive AI + HumanYes (new) *Wave 2
8Schedule & publishTiming + push + multi-system coordinationOptimisation + Automation + AI AgentNoWave 3

Look at the “Appropriate intelligence” column. Across a single marketing process we’ve deployed all six rows of the lens, generative AI, predictive AI, automation, each exactly where its nature of work belongs, and each sequenced by its effect on total throughput.

That is what “the process is sovereign” means in practice. Generative AI turned out to be the star of this process but even here it was one instrument in an orchestra, not the whole band. The leader who walked in saying “let’s put a GenAI agent on our marketing” would have automated the wrong 12%, ignored the compliance bottleneck that actually caps output, and wondered six months later why throughput never moved.

Step 5 — Second-order effects. 

If GenAI raises draft output 5×, the queue moves downstream to compliance review. So the redesign isn’t “generate more copy” — it’s “generate drafts and elevate review”: add an automated brand/claims checker, and keep humans reviewing only the exceptions. Elevate the constraint, then immediately manage the new one.

Step 6 — Friction watch. 

GenAI copy needs an editor. If a human rewrites every draft from scratch, you’ve added friction, not removed it. The design must be human-edits not human-writes, with brand-tuned prompts and templates so drafts land close to final.

Lesson 1: 

When the constraint is language and creative volume, generative AI can genuinely 3–5× throughput. But you must move the human from author to editor, and immediately manage the constraint it pushes downstream.

4.2 Process Analysis 2 : Manufacturing: production-line quality assurance & equipment uptime

Step 1 — Throughput metric. 

The plant exists to do one thing: ship good units. So our throughput metric must capture not just how fast the line runs, but how much sellable, defect-free output it actually produces over the time it was supposed to be running.

The industry-standard measure for exactly this is OEE — Overall Equipment Effectiveness, expressed ultimately as good units per hour. OEE is built from three factors multiplied together:

OEE = Availability × Performance × Quality

Each factor answers a different question about where output is lost:

  • Availability — Of the time the line was scheduled to run, how much was it actually running? It captures losses from breakdowns, unplanned stops, and changeovers. (If the line was scheduled for 10 hours but ran for 7.8, Availability = 78%.)
  • Performance — When it was running, did it run at its rated speed? It captures losses from minor stops, slow cycles, and idling. (If it ran, but at 91% of design speed, Performance = 91%.)
  • Quality — Of the units produced, how many were good the first time? It captures losses from defects, rework, and scrap. (If 87% of units passed without rework, Quality = 87%.)

Step 2 — Decompose OEE. 

This plant runs at 62% OEE. On its own, “62%” tells a leader nothing actionable — it’s a single number hiding three very different stories. Decomposing it is what turns a vague “we’re inefficient” into a precise “here is where we bleed.”

0.78 (Availability) × 0.91 (Performance) × 0.87 (Quality) ≈ 0.62 (62% OEE)

Now we can read each factor against a realistic world-class benchmark (roughly 90%+ on each) to see which one is dragging the whole product down:

OEE componentActualWorld-class benchmarkGapWhat’s actually causing the loss
Availability78%~90%–12 ptsUnplanned downtime — the line stops when equipment fails without warning
Performance91%~95%–4 ptsMinor speed losses — small, tolerable
Quality87%~99%–12 ptsDefect escapes caught late at final inspection, forcing expensive rework

Each row is one of the three multiplied factors. The Actual column is where the plant is today; the benchmark and gap columns show how far each factor sits below what “good” looks like; the final column names the real-world event that causes that gap.

The table’s message is now unmistakable. Performance (91%) is broadly fine — the machines run at close to rated speed, so chasing speed improvements would be optimising a non-problem. The damage is concentrated in two factors: Availability and Quality, each roughly 12 points below benchmark. And because OEE multiplies, closing those two gaps has an outsized effect — lifting Availability and Quality to benchmark alone would move OEE from 62% to roughly 0.90 × 0.91 × 0.99 ≈ 81%, a near one-third jump in sellable output without a single new machine.

This decomposition is Theory of Constraints in action. The single “62%” number gave us nowhere to aim. Broken into its three drivers, it points a spotlight at exactly two constraints — unplanned downtime and late-caught defects — and tells us, before any investment, that any AI or automation investment in speed(Performance) would be money spent on a non-constraint. That is precisely the “widen the road before the toll booth” mistake this commandment warns against.

These two empirically identified constraints — Availability and Quality — are what we now carry into Step 3 and the decision lens.

Step 3 – Find the constraint empirically. 

The OEE decomposition has already narrowed our attention to Availability and Quality. The next step is to verify these findings with operational data rather than assumptions.

The evidence confirms both constraints:

  • Availability: Production logs show repeated episodes of unplanned equipment downtime. Critical machines fail unexpectedly, bringing the production line to a halt and reducing the number of productive operating hours.
  • Quality: Inspection records reveal that a significant proportion of defects are detected only during final inspection. By this stage, defective products have already consumed materials, machine time, and labour, resulting in costly rework and scrap.

Notice what we are not seeing. There is little evidence that machine speed (Performance) is restricting throughput. The machines are generally capable of operating at their designed speed; the real losses come from machines that are not running at all and from defective products that must be reworked or discarded.

The data therefore validates what the OEE decomposition suggested: the primary process constraints are unplanned downtime and late defect detection. These are the constraints we now take into the decision lens to determine the most appropriate intervention.

Step 4 — Decision lens at the constraints.

Recall the empirically found constraints: Availability (78%) killed by unplanned downtime and Quality (87%) killed by defects caught too late. Now walk every step through the lens, watching throughput.

1 — Raw-material intake inspection

Nature of tasksDecision lensThroughput view
Classifying whether incoming material meets spec (vision / measurement pattern-matching)Predictive AI (vision classification) + Automation (dimensional checks)Not the headline constraint, but bad inputs cause downstream defects a Wave-3 quality feeder

2 — Machine setup

Nature of tasksDecision lensThroughput view
Configuring the line to a recipe (deterministic) and choosing optimal parameters (speed vs. wear vs. quality trade-off)Automation (recipe / PLC management) + Optimisation algorithms (optimal parameter sets)Affects Performance; secondary priority

3 — Production run

Nature of tasksDecision lensThroughput view
Deterministic machine controlAutomation (control systems)Already automated; no AI decision here. Resist the urge to “add AI” to a solved step

4 — In-line quality inspection

Nature of tasksDecision lensThroughput view
Detecting defects from images at line speed — pattern classificationPredictive AI(computer vision)Quality constraint (* ). Catching defects in-line instead of at final inspection is where the primary quality gain lives

5 — Defect detection & classification

Nature of tasksDecision lensThroughput view
Classifying which defect type (to drive rework vs. scrap)Predictive AIExtends the Quality-constraint fix (*)

6 — Equipment condition monitoring

Nature of tasksDecision lensThroughput view
Predicting failure from vibration / temperature / current time-seriesPredictive AIAvailability constraint (*). The single highest-value intervention — each avoided stop recovers four hours of full-line output

7 — Maintenance scheduling

Nature of tasksDecision lensThroughput view
Deciding when to service which machine without colliding with production, spares and crewOptimisation algorithms (the schedule) + AI Agent (coordinating maintenance system, production planner, spare-parts inventory)Directly elevates Availability alongside Step 6

8 — Rework / scrap decision

Nature of tasksDecision lensThroughput view
Predict rework success and cost, choose the economically optimal action; edge cases need a personPredictive AI (rework-yield) + Optimisation(cost-minimising choice) + Human(borderline calls)The new constraint (*). Once in-line inspection catches 5× more defects, they pile up here . Elevate rework capacity in the same wave.

9 — Final inspection

Nature of tasksDecision lensThroughput view
Last-line classification + accountable sign-off for shipment / compliancePredictive AI (vision) + Human (release accountability)Becomes lighter once in-line inspection works, since fewer escapes reach it

10 — Packaging

Nature of tasksDecision lensThroughput view
Repetitive deterministic handlingAutomationLow priority

11 — Dispatch

Nature of tasksDecision lensThroughput view
Deterministic execution + route/load optimisation + coordinating WMS / carrier / ERPAutomation + Optimisation (routing) + AI Agent(system coordination)Downstream of the plant constraint; smooth later

So where is Generative AI? Look back, it hasn’t appeared once in the value core. The only honest place for it is at the administrative edges: auto-drafting maintenance work-order summaries, generating shift-handover reports from logs, or a RAG assistant that lets a technician query equipment manuals in plain language. Useful, but peripheral. A leader who walked in demanding “a GenAI agent for the shop floor” would have spent the budget on the 5% that doesn’t move OEE, and left the failing motor unmonitored.

Throughput-first sequencing:

  • Wave 1 : Break Availability: predictive maintenance (6) + scheduling optimisation (7).
  • Wave 2 : Break Quality and its second-order effect: in-line vision inspection (4–5) plus rework capacity (8) in the same wave.
  • Wave 3 : Feed quality upstream: intake inspection (1), setup optimisation (2).
  • Wave 4 : Smooth the tail: dispatch routing (11); leave GenAI reporting as a nice-to-have.

Result table:

#Process stepNature of workAppropriate intelligenceConstraint?Priority
1Raw-material intakeClassify + measurePredictive AI + AutomationNoWave 3
2Machine setupConfigure + optimise paramsAutomation + OptimisationNoWave 3
3Production runDeterministic controlAutomationNoSolved
4In-line inspectionClassify defects (vision)Predictive AIYes *Wave 2
5Defect classificationClassify patternPredictive AIYes *Wave 2
6Condition monitoringPredict failurePredictive AIYes *Wave 1
7Maintenance schedulingOptimise + coordinate systemsOptimisation + AI AgentYesWave 1
8Rework / scrap decisionPredict + optimise + judge edge casesPredictive AI + Optimisation + HumanYes (new) *Wave 2
9Final inspectionClassify + accountable sign-offPredictive AI + HumanNoWave 2
10PackagingRepetitive handlingAutomationNoLow
11DispatchExecute + route + coordinateAutomation + Optimisation + AI AgentNoWave 4

The column is dominated by Predictive AI and Optimisation, with automation at the edges and a couple of legitimate coordination agents. Generative AI ,the technology everyone walked in asking for appears nowhere in the value-creating steps.

Step 5 — Second-order effects. 

Predictive maintenance lifts Availability; vision inspection lifts Quality. But if you catch far more defects in-line, you may now overload the rework station. Plan rework capacity in parallel, or you’ve simply relocated the bottleneck.

Step 6 — Friction watch. 

A vision model that raises false-positive scrap alarms will have operators overriding it within a week — trust dies, and the system is bypassed. Tune for the cost-weighted error the business actually cares about (a missed defect vs. a false alarm), not raw accuracy.

Lesson 2: 

When the constraint is prediction or classification from structured/sensor/image data, predictive AI and automation are cheaper, faster, more accurate, and auditable. Generative AI here isn’t just suboptimal — it’s a category error.

4.3 Process Analysis 3 : HR: handling a formal employee grievance

The process, step by step:

Step 1 — Throughput metric. 

Here’s the trap. The obvious metric is time-to-resolution. But this process does not exist to produce resolutions fast . It exists to produce fair, defensible, trusted outcomes. Speed at the cost of fairness is a catastrophic failure, not a win. The right throughput metric is “fair, legally sound resolutions” , with time as a constraint, not the goal.

This is a lesson in itself: not every process should be optimised for raw throughput. Recognising that is part of AI literacy.

Step 2 — Decompose. 

Where does the cycle time actually go?

StepTimeNature of work
Acknowledge & log1 dayAdministrative
Initial assessment2 daysJudgement + policy lookup
Scheduling interviews5–7 daysCoordination / logistics
Interviews4 daysHuman judgement, empathy
Analysis & outcome5 daysHuman judgement, accountability
Documentation2 daysAdministrative

Step 3 — Find the constraint empirically. 

The time constraint is mundane: interview scheduling (chasing calendars across parties). The value constraints — interviews and analysis — are slow because they should be; they demand human discernment.

Step 4 — Decision lens. 

Apply it honestly, step by step:

Remember the two things that make this process different: the throughput metric is fair, legally sound, trusted resolutions (speed is a constraint, not the goal), and the only genuine time bottleneck is mundane – interview scheduling. Walk the lens honestly.

1 — Grievance received

Nature of tasksDecision lensThroughput view
Intake of a sensitive report, sometimes delivered verbally and emotionallyAutomation (secure digital logging) + Human (empathetic reception when raised in person)Administrative; keep it frictionless and confidential

2 — Acknowledge & log

Nature of tasksDecision lensThroughput view
Deterministic case creation and acknowledgementAutomation (auto-acknowledge, assign case ID) + narrow GenAI (draft the acknowledgement note, human-approved)Pure admin automate it, don’t agonise over it

3 — Initial assessment (formal? which policy?)

Nature of tasksDecision lensThroughput view
Classifying the grievance and retrieving the governing policy, then an accountable decision on how to proceedGenAI (RAG) to surface policy and precedent + Human to decideAI informs; the human decides. Never let retrieval masquerade as judgement

4 — Assign investigator

Nature of tasksDecision lensThroughput view
Allocating a case to an available, appropriately skilled, conflict-free investigatorHuman(conflict-of-interest judgement)Improving efficiency while maintaining fairness.

5 — Gather documents & evidence

Nature of tasksDecision lensThroughput view
Retrieving and organising records; deciding what is relevant and admissibleAutomation (pull from HRIS, email, access logs) + GenAI (organise / summarise) + Human (relevance & admissibility call)Frees investigator time a safe assist

6 — Interview complainant, respondent, witnesses

Nature of tasksDecision lensThroughput view
Empathy, credibility assessment, reading the room — plus the calendar coordination around itHuman, full stop for the interviews; Automation / Optimisation for the schedulingThe real time constraint (*). Interviews should stay slow and human; the calendar coordination is where 5–7 days evaporate — the one place to elevate throughput aggressively

7 — Analyse findings

Nature of tasksDecision lensThroughput view
Weighing conflicting evidence and credibilityHuman judgement, accountableSlow because it must be. GenAI may help organise evidence, but must not weigh it

8 — Determine outcome

Nature of tasksDecision lensThroughput view
High-stakes, legally accountable adjudicationHuman, full stopDo not optimise. Speed here is not a virtue

9 — Communicate decision

Nature of tasksDecision lensThroughput view
Drafting a legally careful, humane communication, then delivering it with empathyGenAI (first draft) + Human (owns every word and the delivery)Assist the drafting; never automate the delivery

10 — Handle appeal

Nature of tasksDecision lensThroughput view
Re-adjudication of the caseHumanSame logic as the core judgement steps

11 — Close & document

Nature of tasksDecision lensThroughput view
Structured case documentation for the record and auditGenAI (draft summary from verified records) + Human (verify) + Automation (archive / retention)Admin tail — assist and file

The hype reflex here is not just wrong, it’s dangerous. “Let an agent triage grievances and recommend outcomes” delegates precisely the judgement, empathy and accountability row — the one row a machine must never own in this context. It imports bias, invents plausible-but-false reasoning, creates legal exposure, and — most destructively — collapses employee trust the instant staff learn a machine judged their complaint. That is a catastrophic second-order effect: fewer people report real issues, problems fester, and litigation risk rises. A faster process nobody trusts has negative net value.

Throughput-first sequencing (here “throughput” means freeing human time for judgement and cutting dead calendar time — never accelerating the judgement itself):

  • Wave 1 — Kill the dead time: automate interview scheduling (6), intake/logging (1–2), archiving (11).
  • Wave 2 — Assist at the edges: GenAI-RAG for policy lookup (3), document organisation (5), communication and case-summary drafts (9, 11) — every output human-verified.
  • Wave 3 — Optimise allocation: investigator assignment (4).
  • Never: automate assessment, interviews, analysis, outcome, or appeal (3-decision, 6, 7, 8, 10).

Result table:

#Process stepNature of workAppropriate intelligenceConstraint?Priority
1Grievance receivedIntake (+ empathy if verbal)Automation + HumanNoWave 1
2Acknowledge & logDeterministic case creationAutomation + narrow GenAINoWave 1
3Initial assessmentRetrieve policy → decideGenAI (RAG) informs + Human decidesNoWave 2
4Assign investigatorAllocate + conflict checkHumanNoWave 3
5Gather evidenceRetrieve/organise + relevance callAutomation + GenAI + HumanNoWave 2
6InterviewsEmpathy/judgement (+ scheduling)Human (scheduling → Automation * )Yes *Wave 1
7Analyse findingsWeigh evidenceHumanNoNever automate
8Determine outcomeAccountable adjudicationHumanNoNever automate
9Communicate decisionDraft + empathetic deliveryGenAI draft + HumanNoWave 2
10Handle appealRe-adjudicationHumanNoNever automate
11Close & documentStructured documentationGenAI + Human + AutomationNoWave 2

Here the column is dominated by Human expertise, with automation and optimisation confined to the logistical edges and GenAI kept firmly on a leash as a drafting and retrieval assistant — never a decision-maker. And crucially, the biggest throughput win came from automating calendars, not from any AI touching the grievance itself.

Step 5 — Second-order effects. 

This is where forcing AI is most destructive. A faster process that employees no longer trust has negative net value: fewer people raise legitimate grievances, issues fester, and litigation risk rises. The second-order damage dwarfs any first-order time saving.

Lesson 3: 

Some processes are dominated by a human-judgement constraint that AI cannot and should not relieve. Here the right move is to automate the logistics around the humans (scheduling, documentation) so they spend more time on judgement , never to automate the judgement itself.

5.0 Conclusion

The three processes, side by side

Marketing contentManufacturing QA/uptimeHR grievance
Throughput metricApproved variants/weekGood units/hour (OEE)Fair, trusted resolutions
Dominant intelligenceGenerative AIPredictive AI + OptimisationHuman expertise
Automation’s roleFormat/publish edgesControl + packaging edgesScheduling + logging edges
Where GenAI belongsThe value coreThe reporting edges onlyOn a tight leash, edges only
Biggest throughput leverCopy generationPredictive maintenanceInterview scheduling
The hype-reflex mistakeWould’ve worked (rare)Slower, costlier, un-auditableDestroys trust, legal risk

One framework. Three honest walk-throughs. Three completely different answers. In every case, the six-row lens did the work — and in only one of the three did generative AI, the technology everyone walks in demanding, turn out to be the right lead instrument.

Now that we have seen three different processes let us now summarise our learning to generate your Commandment 1 checklist.

Before you approve any AI initiative, be able to answer these on one page:

  • ☐ What is the single throughput metric this process exists to produce — and is throughput even the right goal, or is it fairness/quality/safety?
  • ☐ Have I decomposed that metric across every step to see where time, cost, and errors accumulate?
  • ☐ Have I found the constraint empirically — with data on where work queues — rather than guessing?
  • ☐ At the constraint, what kind of work is it? (Generate / predict / automate / judge — apply the lens.)
  • ☐ What is the second-order effect? Where does the bottleneck move next, and have I planned for it?
  • ☐ Where is the friction — review, rework, latency, lost trust — and does it eat my gain?
  • ☐ Am I elevating the constraint, or decorating a non-constraint with fashionable technology?

If you can’t answer these, you’re not ready to choose a model, a framework, or an agent. You’re ready to go back and look at your process.

That is Commandment 1 in a single sentence: 

Decompose the process, find the constraint and let the nature of the work at the constraint, not the fashion of the day, choose the tool.

Next in the series — Commandment 2: “Thou Shalt Not Invoke Agents in Vain.” We’ll take the constraints we’ve now learned to find, and confront the most over-prescribed answer of 2025-2026: the AI Agent. When does a problem genuinely need agentic autonomy — and when is a boring, reliable workflow the smarter, cheaper choice?

Causal Estimation methods for Machine learning and Data Science Part III – Instrument Variable Analysis

1.0 Introduction

In the past two blogs of this series we’ve been discussing causal estimation, a very important subject in data science, we delved into causal estimation using regression method and propensity score matching. Now, let’s venture into the world of instrumental variable analysis—a powerful method for unearthing causal relationships from observational data. Let us look at the structure of this blog.

2.0 Structure

  • Instrument variables – An introduction
  • Instrument variable analysis – The process
  • Implementation of instrument variable analysis from scratch using linear regression
  • Implementation of instrument variable analysis using DoWhy
  • Implementation of instrument variable analysis using Ordinary Least Squares (OLS) Method
  • Conclusion

3.0 Instrument Variables – An introduction

Let us start the explanation on the instrument variable analysis with an example.

We all know education is important, but figuring out how much it REALLY boosts your future earnings is tricky. Its not easy to tell how big a difference an extra year of school makes because we also don’t know how smart someone already is. Some people are just naturally good at stuff, and that might be why they earn more, not just because they went to school longer. This confusing mix makes it hard to see the true effect of education.

But there’s a way out of this impasse!!

Imagine that you have a mechanism to separate the real link between education and earnings while ignoring how smart someone is. That’s what an instrument variable is. It helps us see the clear path between education and income, without getting fooled by other factors. In our example, an instrument variable that could be considered could be something like compulsory schooling laws. These laws force everyone to spend a certain amount of time in school, regardless of their natural talent. So, by studying how those laws affect people’s earnings, we can get a clearer picture of what education itself does, apart from just naturally-smart people earning more.

With this tool, we can finally answer the big question: is education really the key to unlocking a brighter financial future?

4.0 Instrument variable analysis – The process

Here’s how it works:

  1. Effect of instrument on treatment: We analyze how compulsory schooling laws (the instrument variable) affect education (the treatment).
  2. Estimate the effect of education on outcome: We use the information from the above to estimate how education (treatment) actually affects earnings (outcome), ignoring the influence of natural talent (the unobserved confounders).

By using this method, we can finally isolate the true effect of education on earnings, leaving the confusing influence of natural talent behind. Now, we can confidently answer the question:

Does more education truly lead to a brighter financial future?

Remember, this is just one example, and finding the right instrument variable for your situation can be tricky. But with the right tool in hand, you can navigate the maze of confounding factors and uncover the true causal relationships in your data.

Implementation of Causal estimation using Instrument Variables from scratch

Let us now explain the concept through code. First we will use the linear regression method to take you through the estimation process.

To start off, lets generate some synthetic data set which describes the relationship between the variables.

# Importing the necessary packages
import numpy as np
from sklearn.linear_model import LinearRegression

Now let’s create the synthetic dataset

# Generate sample data (replace with your actual data)
n = 1000
ability = np.random.normal(size=n)
compulsory_schooling = np.random.binomial(1, 0.5, size=n)
education = 5 + 2 * compulsory_schooling + 0.5 * ability + np.random.normal(size=n)
earnings = 10 + 3 * education + 0.8 * ability + np.random.normal(size=n)

This section creates simulated data for n individuals (1000 in this case):

  • ability: Represents unobserved individual ability (normally distributed).
  • compulsory_schooling: Binary variable indicating whether someone was subject to compulsory schooling (50% chance).
  • education: Years of education, determined by compulsory schooling (2 years difference), ability (0.5 years per unit), and random error.
  • earnings: Annual earnings, influenced by education (3 units per year), ability (0.8 units per unit), and random error.

We can see that the variables are defined in such a way that education depends on both schooling laws ( instrument variable) and ability ( unobserved confounder) and the earnings ( outcome ) depends on education ( Treatment ) and ability. The outcome is not directly influenced by the instrument variable, which is one of the condition of selecting an instrument variable.

Let us now implement the first regression model.

# Stage 1: Regress treatment (education) on instrument (compulsory schooling)
stage1_model = LinearRegression()
stage1_model.fit(compulsory_schooling.reshape(-1, 1), education)  # Reshape for 2D input
predicted_education = stage1_model.predict(compulsory_schooling.reshape(-1, 1))

This stage uses linear regression to model how compulsory schooling (compulsory_schooling) affects education (education).

In line 13, we reshape the data to the required 2D format for scikit-learn’s LinearRegression model. The model estimates the effect of compulsory schooling on education, isolating the variation in education directly caused by the instrument (compulsory schooling) and removing the influence of ability (confounding variable).

In line 14, The resulting model predicts the “purified” education values (predicted_education) for each individual, eliminating the confounding influence of ability.

Now that we have found the education variable which is precluded of the influence from unobserved confounder ( ability ), we will build the second regression model.

# Stage 2: Regress outcome (earnings) on predicted treatment from stage 1
stage2_model = LinearRegression()
stage2_model.fit(predicted_education.reshape(-1, 1), earnings)  # Reshape for 2D input

This stage uses linear regression to model how the predicted education (predicted_education) affects earnings (earnings).

Again, in line 16 we reshape the data for compatibility with the model and fit the model to estimate the causal effect of education on earnings. Here we use the “cleaned” education values from stage 1 to isolate the true effect, holding the influence of ability constant.

Let us now extract the coefficients of this model. The coefficient of predicted_education represents the estimated change in earnings associated with a one-unit increase in education, adjusting for the confounding effect of ability.

# Print coefficients (equivalent to summary in statsmodels)
print("Intercept:", stage2_model.intercept_)
print("Coefficient of education:", stage2_model.coef_[0])

Intercept (9.901): This indicates the predicted earnings for someone with zero years of education.

Coefficient of education (3.004): This shows that, on average, each additional year of education is associated with an increase of $3.004 in annual earnings, holding ability constant with the help of the instrument.

Let us try to get some intuition on the exercise we just performed. In the first stage, we estimate how changes in the instrument (here, compulsory schooling laws) impact education. Then in the second stage, we examine how changes in education, predicted from the first stage, impact earnings. The first stage helps address the issue of unobserved confounding by using a variable (the instrument) that only affects the outcome through its impact on the treatment variable. Thus we are able to estimate the real effect of education on the earning capacity, by eliminating any influence from the unobserved confounding variables like ability.

We implemented this exercise using linear regression to get an intuitive understanding of what is going on under the hood. Now let us implement the same exercise using DoWhy library.

5.0 Implementation using DoWhy

We will start by importing the required libraries.

from dowhy import CausalModel

We will be using the same data frame which we used earlier. Let us now define the data frame for our analysis.

# Create a pandas DataFrame
data = pd.DataFrame({
    'ability': ability,
    'compulsory_schooling': compulsory_schooling,
    'education': education,
    'earnings': earnings
})

Next let us define the causal model.

# Define the causal model
model = CausalModel(
    data=data,
    treatment='education',
    outcome='earnings',
    instruments=['compulsory_schooling']
)

The above code creates the causal model using DoWhy.

Let’s break down the key components and the processes happening behind the scenes:

model = CausalModel(...): This line initializes a causal model using the DoWhy library.

data: The dataset containing the variables of interest.

treatment='education': Specifies the treatment variable, i.e., the variable that is believed to have a causal effect on the outcome.

outcome='earnings': Specifies the outcome variable, i.e., the variable whose changes we want to attribute to the treatment.

instruments=['compulsory_schooling']: Specifies the instrumental variable(s), if any. In this case, ‘compulsory_schooling’ is used as an instrument.

In the provided code snippet, there is no explicit specification of common causes, which in our case is the variable ‘ability’. The absence of common causes in the CausalModel definition may imply that the common causes are either not considered or are left unspecified. In our case we have said that the common causes or confounders are unobserved. That is why we use the instrument variable ( compulsory_schooling) to negate the effect of unobserved confounders.

When the causal model is defined as shown above, DoWhy performs an identification step where it tries to identify the causal effect using graphical models and do-calculus. It checks if the causal effect is identifiable given the specified variables. To know more about the identification steps you can refer our previous blogs on the subject.

Once identification is successful, the next step is to estimate the causal effect. Let us proceed with the estimation process.

# Identify the causal effect using instrumental variable analysis
identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
estimate = model.estimate_effect(identified_estimand, method_name="iv.instrumental_variable")

Line 17 , identified_estimand = model.identify_effect(proceed_when_unidentifiable=True): In this step, the identify_effect method attempts to identify the causal effect based on the specified causal model. The proceed_when_unidentifiable=True parameter allows the analysis to proceed even if the causal effect is unidentifiable, with the understanding that this might result in less precise estimates.

Line 18 estimate = model.estimate_effect(identified_estimand, method_name="iv.instrumental_variable"): This method takes the identified estimand and specifies the method for estimating the causal effect. In this case, the method chosen is instrumental variable analysis, specified by method_name="iv.instrumental_variable". Instrumental variable analysis helps in addressing potential confounding in observational studies by finding an instrument (a variable that is correlated with the treatment but not directly associated with the outcome) to isolate the causal effect.The intuition for the instrument variable was earlier described when we built the linear regression model.

Finally the estimate object contains information about the estimated causal effect. Let us print the causal effect in our case

# Print the causal effect estimate
print("Causal Effect Estimate:", estimate.value)

From the output we can see that its similar to our implementation using the linear regression method. The idea of implementing the linear regression method is to unravel the intuition which is often hidden in black box implementations like that in the DoWhy package.

Now that we have a fair idea and intuition on what is happening in the instrument variable analysis, let us see one more method of implementation called the two-stage least squares (2SLS) regression method. We will be using the statsmodels library for the implementation.

6.0 Ordinary Least Squares (OLS) Method method

Let us see the full implementation using least squares method.

import numpy as np
import pandas as pd
import statsmodels.api as sm

# Set seed for reproducibility
np.random.seed(42)

# Generate synthetic data
n_samples = 1000

# True coefficients
beta_education = 3.5  # True causal effect of education on earnings
gamma_instrument = 2.0  # True effect of the instrument on education
delta_intercept = 5.0  # Intercept in the second stage equation

# Generate data
instrument_z = np.random.randint(0, 2, size=n_samples)  # Instrument (0 or 1)
education_x = 2 * instrument_z + np.random.normal(0, 1, n_samples)  # Education affected by the instrument
earnings_y = delta_intercept + beta_education * education_x + gamma_instrument * instrument_z + np.random.normal(0, 1, n_samples)

# Create a DataFrame
data = pd.DataFrame({'Education': education_x, 'Earnings': earnings_y, 'Instrument': instrument_z})

# First stage regression: Regress education on the instrument
first_stage = sm.OLS(data['Education'], sm.add_constant(data['Instrument'])).fit()
data['Predicted_Education'] = first_stage.predict()

# Second stage regression: Regress earnings on the predicted education
second_stage = sm.OLS(data['Earnings'], sm.add_constant(data['Predicted_Education'])).fit()

In line 6, we set the seed for reproducibility. Then in lines 12-14, we define the true coefficients for the simulation. This step is done only to compare the final results with the actual coefficients, since we have the luxury of defining the data itself.

In lines 17-19, we generate synthetic data for the analysis. The variables for this data are the following.

  • instrument_z represents the instrument (0 or 1).
  • education_x is affected by the instrument.
  • earnings_y is generated based on the true coefficients and some random noise.

In line 22, we create a DataFrame to hold the simulated data.

In lines 25-26, we perform the first stage regression: regress education on the instrument.

  • sm.OLS: This is creating an Ordinary Least Squares (OLS) regression model. OLS is a method for estimating the parameters in a linear regression model.
  • data['Education']: This is specifying the dependent variable in the regression, which is education (X).
  • sm.add_constant(data['Instrument']): This part is adding a constant term to the independent variable, which is the instrument (Z). The constant term represents the intercept in the linear regression equation.
  • .fit(): This fits the model to the data, estimating the coefficients.

We finally store the predictions in a variable ‘Predicted_Eduction

In the second stage regression in line 29, earnings is regressed on the predicted education from the first stage.This stage estimates the causal effect of education on earnings, considering the predicted education from the first stage.The coefficient of the predicted education in the second stage represents the causal effect.

Let us look at the results from each stage .

# Print results
print("First Stage Results:")
print(first_stage.summary())

print("\nSecond Stage Results:")
print(second_stage.summary())

Let’s interpret the results obtained from both the first and second stages:

First stage results:

Constant (Intercept): The constant term (const) is estimated to be 0.0462, but its p-value (P>|t|) is 0.308, indicating that it is not statistically significant. This suggests that the instrument is not systematically related to the baseline level of education.

Instrument: The coefficient for the instrument is 1.9882, and its p-value is very close to zero (P>|t| < 0.001). This implies that the instrument is statistically significant in predicting education.

R-squared: The R-squared value of 0.497 indicates that approximately 49.7% of the variability in education is explained by the instrument.

F-statistic:The F-statistic (984.4) is highly significant with a p-value close to zero. This suggests that the instrument as a whole is statistically significant in predicting education.

The overall fit of the first stage regression is reasonably good, given the significant F-statistic and the instrument’s significant coefficient.

The coefficient for the instrument (Z) being 1.9882 with a very low p-value suggests a statistically significant relationship between the instrument (compulsory schooling laws) and education (X). In the context of instrumental variable analysis, this implies that the instrument is a good predictor of the endogenous variable (education) and helps address the issue of endogeneity.

The compulsory schooling laws (instrument) affect education levels. The positive coefficient suggests that when these laws are in place, education levels tend to increase. This aligns with the intuition that compulsory schooling laws, which mandate individuals to stay in school for a certain duration, positively influence educational attainment.

In the context of the broader problem—examining whether education causally increases earnings—the significance of the instrument is crucial. It indicates that the laws that mandate schooling have a significant impact on the educational levels of individuals in the dataset. This, in turn, supports the validity of the instrument for addressing the potential endogeneity of education in the relationship with earnings.

Second stage results:

Constant (Intercept): The constant term (const) is estimated to be 5.0101, and it is statistically significant (P>|t| < 0.001). This represents the baseline earnings when the predicted education is zero.

Predicted Education: The coefficient for predicted education is 4.4884, and it is highly significant (P>|t| < 0.001). This implies that, controlling for the instrument, the predicted education has a positive effect on earnings.

R-squared: The R-squared value of 0.605 indicates that approximately 60.5% of the variability in earnings is explained by the predicted education.

F-statistic: The F-statistic (1530.0) is highly significant, suggesting that the model as a whole is statistically significant in predicting earnings.

The overall fit of the second stage regression is good, with significant coefficients for the constant and predicted education.

The coefficient for predicted education is 4.4884, and its high level of significance (P>|t| < 0.001) indicates that predicted education has a statistically significant and positive effect on earnings. In the second stage of instrumental variable analysis, predicted education is used as the variable to estimate the causal effect of education on earnings while controlling for the instrument (compulsory schooling laws).The intercept (baseline earnings) is also significant, representing earnings when the predicted education is zero.

The positive coefficient suggests that an increase in predicted education is associated with a corresponding increase in earnings. In the context of the overall problem—examining whether education causally increases earnings—this finding aligns with our expectations. The positive relationship indicates that, on average, individuals with higher predicted education levels tend to have higher earnings.

In summary, these results suggest that, controlling for the instrument, there is evidence of a positive causal effect of education on earnings in this example.

7.0 Conclusion

In the course of our exploration of causal estimation in the context of the education and earnings we traversed three distinct methods to unravel the causal dynamics:

Implementation from Scratch using Linear Regression: We embarked on the journey of causal analysis by implementing from scratch using linear regression. This method, was aimed to understand the intuition on the use of instrument variable to estimate the causal link between education and earnings.

Dowhy Implementation: Implementation using DoWhy facilitated a structured causal analysis, allowing us to explicitly define the causal model, identify key parameters, and estimate causal effects. The flexibility and transparency offered by DoWhy proved instrumental in navigating the complexities of causal inference.

Ordinary Least Squares (OLS) Method: We explored the OLS method to enrich our toolkit, for instrumental variable analysis. This method introduced a different perspective, by carefully selecting and leveraging instrumental variables. Employing this method were were able to isolate the causal effect of education on earnings.

Instrumental variable analysis, have impact across diverse domains like finance, marketing, retail,manufacturing etc. Instrumental variable analysis comes into play when we’re concerned about hidden factors affecting our understanding of cause and effect.This method ensures that we get to the real impact of changes or decisions without being misled by other influences. Let us look at its use cases in different domains.

Marketing: In marketing, figuring out the real impact of strategies and campaigns is crucial. Sometimes, it gets complicated because there are hidden factors that can cloud our understanding. Imagine a company launching a new ad approach – instrumental variables, like the reach of the ad, can help cut through the noise, letting marketers see the true effects of the campaign on things like customer engagement, brand perception, and, of course, sales.

Finance: In finance understanding why things happen is a big deal. For example assessing how changes in interest rates affect economic indicators. Instrumental variables help us here, making sure our predictions are solid and helping policymakers and investors make better choices.

Retail: In retail it’s not always clear why people buy what they buy. That’s where instrumental variable analysis can be a handy tool for retailers. Whether it’s figuring out if a new in-store gimmick or a pricing trick really works, instrumental variables, like things that aren’t directly related to what’s happening in the store, can help retailers see what’s really driving customer behavior.

Manufacturing: Making things efficiently in manufacturing involves tweaking a lot of stuff. But how do you know if the latest tech upgrade or a change in how you get materials is actually helping? Enter instrumental variable analysis. It helps you separate the real impact of changes in your manufacturing process from all the other stuff that might be going on. This way, decision-makers can fine-tune their production strategies with confidence.

Instrumental variable analysis helps people in these different fields see things more clearly. It’s not fooled by hidden factors, making it a go-to method for getting to the heart of why things happen in marketing, finance, retail, and manufacturing.

That’s a wrap! But the journey continues…

So, we’ve dipped our toes into the fascinating (and sometimes frustrating) world of causal estimation using instrumental variables. It’s a powerful tool, but it’s not a magic bullet.

The world Causal AI and in general AI is ever evolving, and we’re here to stay ahead of the curve. Want to dive deeper, unlock industry secrets, and gain valuable insights?

Then subscribe to our blog and YouTube channel!

We’ll be serving up fresh content regularly, packed with expert interviews, practical tips, and engaging discussions. Think of it as your one-stop shop for all things business, delivered straight to your inbox and screen. ✨

Click the links below to join the community and start your journey to mastery!

YouTube Channel: [Bayesian Quest YouTube channel]

Remember, the more we learn together, the greater our collective success! Let’s grow, connect, and thrive .

P.S. Don’t forget to share this post with your fellow enthusiasts! Sharing is caring, and we love spreading the knowledge.

Unlocking Business Insights: Part II – Analyzing the Impact of a Member Rewards Program Using Causal Analysis

In our last blog, we covered the basics of causal analysis, starting from defining problems to creating simulated data. We explored key concepts like back door, front door, and instrumental variables for handling complex causal relationships. Now, we’re taking the next step, focusing on estimation methods, understanding causal effects, and diving into the world of propensity score estimation. Join us as we delve deeper into causal analysis, applying these concepts to Member Loyalty Programs. In this part of the series, we’ll be tackling the following:

Structure

  • Causal Estimation
    • Deciphering causation. Exploring diverse methods for causal estimation
    • Selection of causal estimation method
  • Estimation of causal effect using propensity score matching
  • Implementing causal estimation using PSM
    • Model fitting
    • Matching
    • Estimation
  • Implementing PSM code from scratch
    • Building propensity model using classification model
    • Matching of groups using Nearest Neighbour
    • Calculating ATT, ATC and ATE
    • Interpretation of results
  • Implementing PSM using DoWhy library
  • Conclusion

1.0 Causal estimation

Now that we’ve tackled, the initial steps in causal analysis — defining the problem, preparing the data, creating causal graphs, and identifying causation ,in our previous blog — it’s time for the next phase: causal estimation. Simply put, this step is about figuring out how much the treatment influences the outcome. Whether we’re studying the impact of a marketing campaign on sales or a new drug on patient health, causal estimation moves us beyond just finding connections. The key features we explored earlier, like defining valid instruments and identifying backdoor and frontdoor paths, play a crucial role in choosing the right methods for estimation. This ensures our estimated causal effects are robust and reliable.

1.1 Deciphering Causation: Exploring Diverse Methods for Causal Estimation

As we delve into estimating causation, we encounter a variety of methods, each tailored to address specific aspects of the relationship between treatment and outcome. Regression Analysis is a foundational approach, using statistical models to untangle treatment effects. Matching Methods come into play for direct comparisons, pairing treated and untreated units based on similar covariate profiles. Propensity Score Matching, a subset of matching, estimates the likelihood of receiving treatment based on observed covariates, leading to more accurate matches. Instrumental Variable (IV) Analysis, which we introduced during causal identification, reappears to handle endogeneity concerns. Difference-in-Differences (DiD), a temporal method, contrasts changes in treatment and control groups over time. Regression Discontinuity Design (RDD) excels when treatment hinges on a threshold, revealing causal effects around that point. This array of causal estimation methods provides flexibility, with each being a powerful tool in deciphering causation from correlation for more accurate insights. To learn more on different causal estimation methods, you can refer to some of the previous blogs in our series

1.2 Selection of causal estimation method

In the context of the membership program, individuals self-select into the treatment group (those who signed up for the program) or the control group (those who did not sign up). This self-selection introduces potential confounding, as individuals who choose to sign up for the program may have different characteristics and behaviors compared to those who do not sign up. For example, individuals who are more loyal or already have higher spending patterns may be more inclined to sign up for the program.

Based on our business context Propensity score matching (PSM) can be an appropriate method for estimating the causal effect of a membership program as the program’s signup is not randomized and based on observational data. PSM aims to reduce selection bias and create comparable treatment and control groups by matching individuals with similar propensity scores.

To address the confounding, referred to in the first paragraph, PSM estimates the propensity scores, which represent the probability of an individual signing up for the program given their observed covariates. The propensity scores are then used to match individuals in the treatment group with individuals in the control group who have similar scores. By creating comparable groups, PSM reduces the selection bias and allows for a more valid estimation of the causal effect.

PSM provides several advantages in estimating the causal effect of the membership program. Firstly, it allows for the utilization of observational data, which is often more readily available compared to experimental data from randomized controlled trials. Secondly, PSM can handle a large number of covariates, making it suitable for complex datasets with multiple confounding factors. Thirdly, PSM does not require assumptions about the functional form of the relationship between covariates and the outcome, providing flexibility in modeling.

Now that we have selected an appropriate method for estimating the causal effect let us go ahead and estimate the effect.

2.0 Estimation of Causal Effect using PSM

Estimating the effect in causal analysis refers to the process of quantifying the causal relationship or the impact of a particular treatment or intervention on an outcome of interest. Causal analysis aims to answer questions such as

“What is the effect of X on Y?” or

“Does the treatment T cause a change in the outcome Y?”

Estimating the effect in our context entails quantifying the causal relationship or the impact of a membership program (treatment ) on customer spending patterns (outcome of interest). The goal is to determine whether the membership program causes a change in the customers’ post-spending behavior.

To estimate this effect, causal analysis aims to isolate the causal relationship between the membership program (treatment) and post-spends from other factors that may influence customer spending. These factors can include variables such as customers’ purchasing habits prior to signing up for the program, seasonality, and other unobserved factors. Addressing these potential confounding variables is crucial to obtain an accurate estimation of the causal effect of the membership program on post-spends. In our case we only have a single confounding factor which is the sign up month variable. We will be adjusting the effect of the confounding variable in our estimation.

By carefully accounting for confounding variables through methods like propensity score matching the causal analysis aims to provide reliable estimates of the treatment effect. These estimates help answer questions about the effectiveness of the membership program in influencing customer spending patterns and provide valuable insights for decision-making and program evaluation. Let us now look at the steps to estimate the effect using propensity score matching. The estimation would entail the following steps.

3.0 Steps in implementing causal estimation using PSM

Propensity Score Matching (PSM) unfolds in three pivotal steps. The journey begins with model fitting, often employing logistic regression to craft propensity scores that signify the likelihood of receiving treatment based on observed covariates. Following this, the matching phase seeks balance between treated and control groups, ensuring a fair and unbiased comparison. This involves pairing individuals with similar or identical propensity scores, akin to creating a controlled experiment from observational data. Finally, we estimate effects, scrutinizing the outcomes for the matched pairs to discern the causal impact of the treatment variable.

Model Fitting

  • Fit a model (e.g., logistic regression) to estimate the propensity scores. The model predicts the probability of receiving the treatment based on the observed covariates.

Matching:

  • Match each treated unit with one or more control units from the control group who have similar or close propensity scores.
  • The matching process aims to balance the covariates between the treatment and control groups, making them comparable.

Estimation:

  • Calculate the average treatment effect (ATE) or the average treatment effect on the treated (ATT) using the matched data.
  • The treatment effect is estimated by comparing the outcomes between the treated and matched control units.

We will explain each of these steps when we implement them. To implement these steps let us get back to the data frame we created in the previous blog and the separate out the data for the treatment, outcome and confounding variables

# Separating the treatment, outcome and confounding data
treatment_name = ['treatment']
outcome_name = ['post_spends']
common_cause_name = ['signup_month']
# Extracting the relevant data
treatment = df_i_signupmonth[treatment_name]
outcome = df_i_signupmonth[outcome_name]
common_causes = df_i_signupmonth[common_cause_name]

Figure 1: Snap shot of the treatment, outcome and common causes data

Let us now define the propensity score model, which we will fit with a logistic regression model.

from sklearn import linear_model
# Defining the propensity score model
propensity_score_model = linear_model.LogisticRegression()

Let us take a step back and understand the intuition behind defining a logistic regression model as our propensity score model.

The choice of a logistic regression model as the propensity score model in the context of the membership program is to estimate the probability of customers signing up for the program (treatment) based on their characteristics (common causes).

In causal analysis, the propensity score is defined as the conditional probability of receiving the treatment given the observed covariates. In the context of estimating the effect in causal analysis, the conditional probability helps address the issue of confounding variables. Confounding occurs when there are factors or variables that are associated with both the treatment and the outcome, and they distort the estimation of the causal effect. By conditioning on or adjusting for the common causes, we aim to create comparable groups of treated and control individuals with similar characteristics.

The propensity score model, such as logistic regression, estimates this conditional probability by modeling the relationship between the common causes and the probability of treatment.In the context of the membership program, by estimating the propensity score, we can adjust for the potential confounding variable (sign-up month) which may influence both the treatment assignment (being a member) and the outcome of interest (post spend). Confounding occurs when there are factors that are associated with both the treatment and the outcome, and failing to account for them can lead to biased estimates of the treatment effect.

Using the propensity score, we can match individuals who have similar probabilities of signing up for the program, effectively creating comparable groups in terms of their likelihood of being members. This matching process ensures that any differences in post spend between the treatment and control groups can be attributed primarily to the treatment itself, rather than the confounding effect of sign-up month. By isolating the causal effect of the membership program through propensity score matching, we can more accurately estimate how the program influences post spend for customers who have signed up.

Let us now estimate the propensity scores by fitting the model with the common causes and the treatment variable. Before we actually fit the model we have to reformat the data sets a bit.

# Reformatting the common causes and treatment variables
common_causes = pd.get_dummies(common_causes, drop_first=True)
treatment_reshaped = np.ravel(treatment)
# Fit the model using these variables
propensity_score_model.fit(common_causes, treatment_reshaped)
# Getting the propensity scores by predicting with the model
df_i_signupmonth['propensity_scores'] = propensity_score_model.predict_proba(common_causes)[:, 1]
df_i_signupmonth

Line 13 of code uses the pd.get_dummies() function to convert the common_causes variable into a one-hot encoded variable. This means that each of the categorical variables in the common_causes variable will be converted into a new binary variable. The drop_first=True argument tells the pd.get_dummies() function to drop the first level of the categorical variable. This is done because the first level is usually the reference level, and it does not provide any additional information.

Line 14 uses the np.ravel() function to convert the treatment variable into a 1D array. This is necessary because the propensity_score_model.fit() function expects a 1D array as the dependent variable.
Line 16 uses the propensity_score_model.fit() function to fit the model to the common_causes and treatment_reshaped variables. This function will estimate the coefficients of the model, which will be used to predict the propensity scores.

Line 18 uses the propensity_score_model.predict_proba() function to predict the propensity scores for each individual in the df_i_signupmonth DataFrame. The [:, 1] slice tells the propensity_score_model.predict_proba() function to return only the probability of receiving the treatment, which is the second column of the output array.

The new data frame with the prediction will be as below

Figure 2 : Dataframe with the propensity score predicted

From the data frame, the output propensity_scores represents the estimated probability of an individual signing up for the program given their observed characteristics (common causes). A higher propensity score indicates a higher probability of signing up for the membership program, and vice versa. By fitting the model and predicting the propensity scores, we obtain a quantitative measure of the likelihood of an individual being a member based on their observed characteristics.

These propensity scores are valuable in causal analysis because they allow for matching or stratification of individuals who have similar probabilities of treatment. By grouping individuals with similar propensity scores, we can create comparable treatment and control groups that are balanced in terms of their observed characteristics. This enables more accurate estimation of the causal effect by isolating the impact of the treatment (membership program) from other confounding factors. Let us now start the process

# Seperate the treated and control groups
treated = df_i_signupmonth.loc[df_i_signupmonth[treatment_name[0]] == 1]
control = df_i_signupmonth.loc[df_i_signupmonth[treatment_name[0]] == 0]

Figure 3: Treated and Control groups with predicted propensity score

From the separate data frames of treated and control, you can see the difference in the propensity scores. The treated group has much higher likelihood than the control group. Next we will find the neighbours for the treated and control groups respectively to find individuals of similar propensities.

In propensity score matching, the goal is to identify individuals in the control group who are similar to those in the treatment group based on their propensity scores. This is done to create a matched comparison group that closely resembles the treated group in terms of their likelihood of receiving the treatment.

# Import the required libraries
from sklearn.neighbors import NearestNeighbors
# Fit the nearest neighbour on the control group ( Have not signed up) propensity score
control_neighbors = NearestNeighbors(n_neighbors=1, algorithm="ball_tree").fit(control["propensity_scores"].values.reshape(-1, 1))
# Find the distance of the control group to each member of the treated group ( Individuals who signed up)
distances, indices = control_neighbors.kneighbors(treated["propensity_scores"].values.reshape(-1, 1))

Line 26 fits a nearest neighbors model on the control group’s propensity scores. This model will find the nearest neighbors of each individual in the control group, based on their propensity scores.

Line 28 then finds the distance of the control group to each member of the treated group. This is done by using the nearest neighbors model that was fit on the control group. The distance is calculated by finding the Euclidean distance between the propensity scores of the individuals in the control group and the propensity scores of the individuals in the treated group.

The reason why we fit the nearest neighbors model on the control group and then find its distance on the treated group is because we want to find the individuals in the control group who are most similar to the individuals in the treated group. By fitting the model on the control group, we can ensure that the distances are calculated based on the propensity scores of the control group.

This is important because we want to match individuals who are similar in terms of their propensity scores, so that we can control for confounding variables. If we were to fit the nearest neighbors model on the treated group, we would be matching individuals who are similar in terms of their propensity scores, but who may not be similar in terms of other confounding variables.

Having found the indices of the individuals in the control group, we will be able to calculate the average treatment effect of the treated ( ATT ).

ATT refers to the average causal effect of the treatment on the treated group. It estimates the average difference in the outcome variable between the treated group (those who received the treatment, in this case, signed up for the membership program) and their matched counterparts in the control group (those who did not receive the treatment).

The calculation of ATT involves comparing the outcomes of the treated group with their nearest neighbors in the control group, who have similar propensity scores. By matching individuals based on their propensity scores, we aim to create balanced comparison groups, where the only systematic difference between the treated and control group is the treatment itself. Let us look at how this is done

# Calculation of the ATT
att = 0
numtreatedunits = treated.shape[0]
for i in range(numtreatedunits):
  treated_outcome = treated.iloc[i][outcome_name].item()
  control_outcome = control.iloc[indices[i][0]][outcome_name].item()
  att += treated_outcome - control_outcome
att /= numtreatedunits
print('Average treatment effect of treated',att)

The provided code snippet calculates the ATT by computing the difference in outcomes between the treated group and their matched counterparts in the control group.

Line 32 iterates over each individual in the treated group and retrieves their outcome value. In lines 33-34, using the indices obtained from the nearest neighbor search, the corresponding control unit is identified and its outcome value is retrieved. The difference between the treated outcome and the matched control outcome is then computed and added to the ATT variable, as shown in line 35. This process is repeated for each treated individual. The resulting ATT value represents the average difference in outcomes between the treated and matched control group, providing an estimate of the causal effect of the membership program on the treated individuals. Finally the ATT is calculated by dividing by the number of treated individuals. We get a value of 93.45 for ATT.

An Average Treatment Effect of Treated (ATT) value of 93.45 suggests that, on average, individuals who received the treatment experienced an increase or improvement in the outcome variable by 93.45 units compared to if they had not received the treatment. In other words, the treatment is associated with a positive impact on the outcome.

ATT is relevant because it provides an estimate of the causal effect of the treatment specifically for those who received it. It helps us understand the impact of the membership program on the treated individuals’ outcomes, such as post-spending behavior, by accounting for potential confounding factors through propensity score matching.

Similarly let us calculate ATC which is the average treatment effect on the control group.

# Computing ATC
treated_neighbors = NearestNeighbors(n_neighbors=1, algorithm="ball_tree").fit(treated["propensity_scores"].values.reshape(-1, 1))
distances, indices = treated_neighbors.kneighbors(control["propensity_scores"].values.reshape(-1, 1))
# Calculating ATC from the neighbours of the control group
atc = 0
numcontrolunits = control.shape[0]
for i in range(numcontrolunits):
  control_outcome = control.iloc[i][outcome_name].item()
  treated_outcome = treated.iloc[indices[i][0]][outcome_name].item()
  atc += treated_outcome - control_outcome
atc /= numcontrolunits
print('Average treatment effect on control',atc)

We follow a similar process to find the ATC. Here the nearest neighbour is first fitted on the treatment group propensity score. Then the distance or similarity of each individual in the control group to a corresponding individual in the treated group is found out. After finding the neighbours the calculation of ATC is done similar to what we did for ATT.

Having found both ATT and ATC , we are in a position to calculate the estimate for Average Treatment Effect (ATE).

To calculate the ATE , we combine the ATT and ATC weighted by their respective proportion. The ATE represents the average causal effect of the treatment across both the treated and control groups.

The ATE can be calculated using the following formula:

ATE = (ATT * proportion of treated) + (ATC * proportion of control).

Let us now calculate the ATE

# Calculation of Average Treatment Effect
ate = (att * numtreatedunits + atc * numcontrolunits) / (numtreatedunits + numcontrolunits)
print('Average treatment effect',ate)

In the context of the membership program, the ATE holds significant relevance in understanding the impact of the program on customer spending behavior. Through causal analysis, we can estimate the ATE to assess the average causal effect of the program on post-spends. This involves considering factors such as the treatment group (customers who signed up for the program) and the control group (customers who did not sign up) while accounting for potential confounding variables.

By estimating the Average Treatment Effect on the Treated (ATT) and the Average Treatment Effect on the Control (ATC), we can gain valuable insights. A positive ATT would indicate that customers who signed up for the membership program have higher post-spends compared to those who did not sign up. Conversely, a negative ATT would suggest that signing up for the program leads to lower post-spends. The ATC provides a counterfactual comparison, indicating the outcomes that customers in the control group would have had if they had signed up for the program.

The ATE serves as a crucial measure to evaluate the overall impact of the membership program. A positive ATE would suggest that, on average, the program has a positive causal effect on customer post-spends. Conversely, a negative ATE would indicate a negative average causal effect. These findings help stakeholders assess the effectiveness of the program and make informed decisions regarding its implementation and continuation.

Implementing Causal analysis using do-why

In the last blog of the series we dealt with the processes involved in the causal identification namely, creating the causal graph, and then identifying different paths through which causal effect can flow like, back door paths, front door paths and instrumental variables. In our manual analysis of the causal graph we identified the presence of both back door and instrumental variables. Our causal graph did not have a front door variable. We did all the identification manually. We can implement all those processes in do-why also. Let us now see how the identification process can be done using do-why.

# Identification process
identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
print(identified_estimand)

The output generated from this process is as shown below. The output describes the various estimand’s or paths which are relevant. We can see that both the back door path and the instrumental variables have been identified.

Figure 4: Estimand for the causal analysis

Let us now understand above estimands and the different expressions used

Estimand 1 ( Back door )

Estimand Expression: This represents the causal effect of treatment on post_spends, adjusted for signup_month.

This expression is calculating the derivative of the expected value of post_spends with respect to the treatment variable treatment while controlling for the variable signup_month. In simpler terms, it’s looking at how the average post_spends changes when you change the treatment, considering the influence of signup_month to account for potential confounding.

Estimand Assumption 1 (Unconfoundedness): Assumes that there are no unobserved confounders (U) that simultaneously affect the treatment and outcome.The assumption of unconfoundedness is a fundamental requirement for making causal inferences using observational data. Let’s break down the statement:

This part of the assumption is saying that there are no unobserved confounders (U) that directly influence the treatment assignment. In other words, any factor that might influence both the treatment assignment and the outcome is already observed and included in the variables.

Similarly, there are no unobserved confounders that directly influence the outcome variable (post_spends).

Now, the main part of the assumption:

This is asserting that, conditional on treatment assignment (treatment), the month of signup (signup_month), and any observed variables (U), the distribution of post_spends is the same as if we condition only on treatment and signup_month. In simpler terms, it’s saying that, given what we know (treatment assignment, signup month, and any observed factors), the unobserved factors (represented by U) do not introduce bias or confounding.

Estimand 2 (Instrumental Variable):

Figure 5: Estimand expressions

This expression involves more complex calculus but, in essence, it’s estimating the causal effect of the treatment on post_spends using pre_spends and Z as instrumental variables. It’s essentially calculating the ratio of changes in post_spends with respect to changes in treatment, adjusted for changes in pre_spends and Z. The inverse of this is taken to estimate the causal effect.

Estimand Assumption 1: As-if-random

This assumption is related to the instrumental variable (Z). It’s asserting that if there are unobserved factors (U) that influence post_spends (as indicated by �⟶→ ⁣→U⟶→→​), then those unobserved factors are not related to both the instrumental variable (Z) and the variables we’re controlling for (pre_spends). In other words, the instrumental variable is not correlated with the unobserved factors influencing the outcome, ensuring that it acts as a good instrument.

Estimand Assumption 2: Exclusion

This assumption is crucial for instrumental variables. It states that the instrumental variable (Z) and the variable we’re controlling for (pre_spends) do not have a direct effect on the outcome variable (post_spends). The idea is that the only influence these variables have on the outcome is through their impact on the treatment (treatment).

These assumptions ensure that the instrumental variable is a valid instrument for estimating the causal effect of the treatment on the outcome. The first part ensures that the instrumental variable is not correlated with unobserved factors affecting the outcome, and the second part ensures that the instrumental variable only affects the outcome through its impact on the treatment, not directly. Violations of these assumptions could lead to biased estimates.

Estimand 3 (Frontdoor):

No expression is provided in your example because DoWhy did not find a valid front door path.

The above are the processes which happen in the identification step. Once the identification process is complete we go on to the estimation method which we will see next.

# Estimation process
estimate = model.estimate_effect(identified_estimand,
                                 method_name="backdoor.propensity_score_matching",
                                target_units="ate")
print(estimate)

Let us look at the outputs and then unravel its content.

Figure 6: Estimand expressions

The above identified estimand expression aims to capture the causal effect of the treatment variable on post-spending while controlling for the covariate signup_month. The expression represents the derivative of the expected post-spending with respect to the treatment. The assumption of unconfoundedness ensures that there are no unobserved confounders affecting both the treatment assignment and post-spending.

The mean value of 112.27 for the Average Treatment Effect suggests that, on average, the treatment is associated with an increase of approximately 112.27 units in post-spending compared to the control group. Now in our manual method the estimate came to around 95. There is slight difference in both which can be attributed to the difference in random generation of data. However the direction in both the methods is the same.

Conclusion

In this dual-series exploration of causal analysis within the context of our loyalty membership program, we embarked on a comprehensive journey from the foundational principles to the advanced techniques that underpin causal inference. Our journey began with an elucidation of causal analysis, dissecting Average Treatment Effects (ATE), front door, back door, and instrumental variables. We navigated through the landscape of causal graphs, unraveling the relationships and dependencies that characterize our loyalty program dynamics. The second part of our exploration delved into causal identification and estimation, where we meticulously defined our causal questions and applied sophisticated methods to estimate causal effects. These blogs collectively provide a holistic understanding of the intricacies involved in discerning causation from correlation, equipping us with powerful tools to uncover the true impact of our loyalty membership program on customer behavior. As we conclude this series, we’ve not only enhanced our theoretical grasp of causal analysis but have also gained practical insights that can be applied across various domains, illuminating the path toward more informed decision-making in loyalty program management.

“Discover Data Science Wonders: Subscribe Now!”

Embark on a journey through the fascinating world of data science with our blog!

Whether you’re a data enthusiast or just starting, we simplify complex concepts, turning data science into a delightful experience.

Subscribe today to unravel the mysteries and gain insights.

But there’s more!

Join our YouTube channel for visual learning adventures.

Dive into data with us and make learning simple and fun.

Subscribe for your dose of data magic today! 🚀✨

Building Self Learning Recommendation system – V : Prototype Phase II : Self Learning Implementation

This is the fifth post of our series on building a self learning recommendation system using reinforcement learning. This post of the series builds on the previous post where we segmented customers using RFM analysis. This series consists of the following posts.

  1. Recommendation system and reinforcement learning primer
  2. Introduction to multi armed bandit problem
  3. Self learning recommendation system as a K-armed bandit
  4. Build the prototype of the self learning recommendation system : Part I
  5. Build the prototype of the self learning recommendation system: Part II ( This post )
  6. Productionising the self learning recommendation system: Part I – Customer Segmentation
  7. Productionising the self learning recommendation system: Part II – Implementing self learning recommendation
  8. Evaluating different deployment options for the self learning recommendation systems.

Introduction

In the last post we saw how to create customer segments from transaction data. In this post we will use the customer segments to create states of the customer. Before making the states let us make some assumptions based on the buying behaviour of customers.

  1. Customers in the same segment have very similar buying behaviours
  2. The second assumption we will make is that buying pattern of customers vary accross the months. Within each month we are assuming that the buying behaviour within the first 15 days is different from the buying behaviour in the next 15 days. Now these assumptions are made only to demonstrate how such assumptions will influence the creation of different states of the customer. One can still go much more granular with assumptions that the buying pattern changes every week in a month, i.e say the buying pattern within the first week will be differnt from that of the second week and so on. With each level of granularity the number of states required will increase. Ideally such decisions need to be made considering the business dynamics and based on real customer buying behaviours.
  3. The next assumption we will be making is based on the days in a week. We make an assumption that buying behaviours of customers during different days of a week also varies.

Based on these assumptions, each state will have four tiers i.e

Customer segment >> month >> within first 15 days or not >> day of the week.

Let us now see how this assumption can be carried forward to create different states for our self learning recommendation system.

As a first step towards creation of states, we will create some more variables from the existing variables. We will be using the same dataframe we created till the segmentation phase, which we discussed in the last post.

# Feature engineering of the customer details data frame
# Get the date  as a seperate column
custDetails['Date'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%d"))
# Converting date to float for easy comparison
custDetails['Date']  = custDetails['Date'] .astype('float64')
# Get the period of month column
custDetails['monthPeriod'] = custDetails['Date'].apply(lambda x: int(x > 15))

custDetails.head()

Let us closely look at the changes incorporated. In line 3, we are extracting the date of the month and then converting them into a float type in line 5. The purpose of taking the date is to find out which of these transactions have happened before 15th of the month and which after 15th. We extract those details in line 7, where we create a binary points ( 0 & 1) as to whether a date falls in the last 15 days or the first 15 days of the month. Now all data points required to create the state is in place. These individual data points will be combined together to form the state ( i.e. Segment-Month-Monthperiod-Day ). We will getinto nuances of state creation next.

Initialization of values

When we discussed about the K armed bandit in post 2, we saw the functions for generating the rewards and value. What we will do next is to initialize the reward function and the value function for the states.A widely used method for finding the value function and the reward function is to intialize those values to zero. However we already have data on each state and the product buying frequency for each of these states. We will aggregate the quantities of each product as per the state combination to create our initial value functions.

# Aggregate custDetails to get a distribution of rewards
rewardFull = custDetails.groupby(['Segment','Month','monthPeriod','Day','StockCode'])['Quantity'].agg('sum').reset_index()

rewardFull

From the output, we can see the state wise distribution of products . For example for the state Q1_April_0_Friday we find that the 120 quantities of product ‘10002’ was bought and so on. So the consolidated data frame represents the propensity of buying of each product. We will make the propensity of buying the basis for the initial values of each product.

Now that we have consolidated the data, we will get into the task of creating our reward and value distribution. We will extract information relevant for each state and then load the data into different dictionaries for ease of use. We will kick off these processes by first extracting the unique values of each of the components of our states.

# Finding unique value for each of the segment 
segments = list(rewardFull.Segment.unique())
print('segments',segments)
months = list(rewardFull.Month.unique())
print('months',months)
monthPeriod = list(rewardFull.monthPeriod.unique())
print('monthPeriod',monthPeriod)
days = list(rewardFull.Day.unique())
print('days',days)

In lines 16-22, we take the unique values of each of the components of our state and then store them as list. We will use these lists to create our reward an value function dictionaries . First let us create dictionaries in which we are going to store the values.

# Defining some dictionaries for storing the values
countDic = {} # Dictionary to store the count of products
polDic = {} # Dictionary to store the value distribution
rewDic = {} # Dictionary to store the reward distribution
recoCount = {} # Dictionary to store the recommendation counts

Let us now implement the process of initializing the reward and value functions.

for seg in segments:
    for mon in months:
        for period in monthPeriod:
            for day in days:
                # Get the subset of the data
                subset1 = rewardFull[(rewardFull['Segment'] == seg) & (rewardFull['Month'] == mon) & (
                            rewardFull['monthPeriod'] == period) & (rewardFull['Day'] == day)]                
                # Check if the subset is valid
                if len(subset1) > 0:
                    # Iterate through each of the subset and get the products and its quantities
                    stateId = str(seg) + '_' + mon + '_' + str(period) + '_' + day
                    # Define a dictionary for the state ID
                    countDic[stateId] = {}                    
                    for i in range(len(subset1.StockCode)):
                        countDic[stateId][subset1.iloc[i]['StockCode']] = int(subset1.iloc[i]['Quantity'])

Thats an ugly looking loop. Let us unravel it. In lines 30-33, we implement iterative loops to go through each component of our state, starting from segment, month, month period and finally days. We then get the data which corresponds to each of the components of the state in line 35. In line 38 we do a check to see if there is any data pertaining to the state we are interested in. If there is valid data, then we first define an ID for the state, by combining all the components in line 40. In line 42, we define an inner dictionary for each element of the countDic, dictionary. The key of the countDic dictionary is the state Id we defined in line 40. In the inner dictionary we store each of the products as its key and the corresponding quantity values of the product as its values in line 44.

Let us look at the total number of states in the countDic

len(countDic)

You will notice that there are 572 states formed. Let us look at the data for some of the states.

stateId = 'Q4_September_1_Wednesday'
countDic[stateId]

From the output we can see how for each state, the products and its frequency of purchase is listed. This will form the basis of our reward distribution and also the value distribution. We will create that next

Consolidation of rewards and value distribution

from numpy.random import normal as GaussianDistribution
# Consolidate the rewards and value functions based on the quantities
for key in countDic.keys():    
    # First get the dictionary of products for a state
    prodCounts = countDic[key]
    polDic[key] = {}
    rewDic[key] = {}    
    # Update the policy values
    for pkey in prodCounts.keys():
        # Creating the value dictionary using a Gaussian process
        polDic[key][pkey] = GaussianDistribution(loc=prodCounts[pkey], scale=1, size=1)[0].round(2)
        # Creating a reward dictionary using a Gaussian process
        rewDic[key][pkey] = GaussianDistribution(loc=prodCounts[pkey], scale=1, size=1)[0].round(2)

In line 50, we iterate through each of the states in the countDic. Please note that the key of the dictionary is the state. In line 52, we store the products and its counts for a state, in another variable prodCounts. The prodCounts dictionary has the the product id as its key and the buying frequency as the value,. Lines 53 and 54, we create two more dictionaries for the value and reward dictionaries. In line 56 we loop through each product of the state and make it the key of the inner dictionaries of reward and value dictionaries. We generate a random number from a Gaussian distribution with the mean as the frequency of purchase for the product . We store the number generated from the Gaussian distribution as values for both rewards and value function dictionaries. At the end of the iterations, we get a distribution of rewards and value for each state and the products within each state. The distribution would be centred around the frequency of purchase of each of the product under the state.

Let us take a look at some sample values of both the dictionaries

polDic[stateId]
rewDic[stateId]

We have the necessary ingradients for building our selflearning recommendation engine. Let us now think about the actual process in an online recommendation system. In the actual process when a customer visits the ecommerce site, we first need to understand the state of that customer which will be the segment of the customer, the currrent month, which half of the month the customer is logging in and also the day when the customer is logging in. These are the information we would require to create the states.

For our purpose we will simulate the context of the customer using random sampling

Simulation of customer action

# Get the context of the customer. For the time being let us randomly select all the states
seg = sample(['Q1','Q2','Q3','Q4'],1)[0] # Sample the segment
mon = sample(['January','February','March','April','May','June','July','August','September','October','November','December'],1)[0] # Sample the month
monthPer = sample([0,1],1)[0] # sample the month period
day = sample(['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],1)[0] # Sample the day
# Get the state id by combining all these samples
stateId = str(seg) + '_' +  mon + '_' + str(monthPer) + '_' + day
print(stateId)

Lines 64-67, we sample each component of the state and then in line 68 we combine them to form the state id. We will be using the state id for the recommendation process. The recommendation process will have the following step.

Process 1 : Initialize dictionaries

A check is done to find if the value of reward dictionares which we earlier defined has the states which we sampled. If the state exists we take the value dictionary corresponding to the sampled state, if the state dosent exist, we initialise an empty dictionary corresponding to the state. Let us look at the function to do that.

def collfinder(dictionary,stateId):
    # dictionary ; This is the dictionary where we check if the state exists
    # stateId : StateId to be checked    
    if stateId in dictionary.keys():        
        mycol = {}
        mycol[stateId] = dictionary[stateId]
    else:
        # Initialise the state Id in the dictionary
        dictionary[stateId] = {}
        # Return the state specific collection
        mycol = {}
        mycol[stateId] = dictionary[stateId]
        
    return mycol[stateId],mycol,dictionary

In line 71, we define the function. The inputs are the dictionary the state id we want to verify. We first check if the state id exists in the dictionary in line 74. If it exists we create a new dictionary called mycol in line 75 and then load all the products and its count to mycol dictionary in line 76.

If the state dosent exist, we first initialise the state in line 79 and then repeat the same processes as of lines 75-76.

Let us now implement this step for the dictionaries which we have already created.

# Check for the policy Dictionary
mypolDic,mypol,polDic = collfinder(polDic,stateId)

Let us check the mypol dictionary.

mypol

We can see the policy dictionary for the state we defined. We will now repeat the process for the reward dictionary and the count dictionaries

# Check for the Reward Dictionary
myrewDic, staterew,rewDic = collfinder(rewDic,stateId)
# Check for the Count Dictionary
myCount,quantityDic,countDic = collfinder(countDic,stateId)

Both these dictionaries are similar to the policy dictionary above.

We also will be creating a similar dictionary for the recommended products, to keep count of all the products which are recommended. Since we havent created a recommendation dictionary, we will initialise that and create the state for the recommendation dictionary.

# Initializing the recommendation dictionary
recoCountdic = {}
# Check the recommendation count dictionary
myrecoDic,recoCount,recoCountdic = collfinder(recoCountdic,stateId)

We will now get into the second process which is the recommendation process

Process 2 : Recommendation process

We start the recommendation process based on the epsilon greedy method. Let us define the overall process for the recommendation system.

As mentioned earlier, one of our basic premise was that customers within the same segment have similar buying propensities. So the products which we need to recommend for a customer, will be picked from all the products bought by customers belonging to that segment. So the first task in the process is to get all the products relevant for the segment to which the customer belongs. We sort the products, in descending order, based on the frequency of product purchase.

Implementing the self learning recommendation system using epsilon greedy process

Next we start the epsion greedy process as learned in post 2, to select the top n products we want to recommend. To begin this process, we generate a random probability distribution value. If the random value is greater than the epsilon value, we pick the first product in the sorted list of products for the segment. Once a product is picked we remove it from the list of products from the segment to ensure that we dont pick it again. This process as we learned when we implemented K-armed bandit problem, is the exploitation phase.

The above was a case when the random probability number was greater than the epsilon value, now if the random probability number is less than the epsilon value, we get into the exploration phase. We randomly sample a product from the universe of products for the segment. Here again we restrict our exploration to the universe of products relevant for the segment. However one could design the exploration ourside the universe of the segment and maybe explore from the basket of all products for all customers.

We continue the exploitation and exploration process till we get the top n products we want. We will look at some of the functions which implements this process.

# Create a function to get a list of products for a certain segment
def segProduct(seg, nproducts,rewardFull):
    # Get the list of unique products for each segment
    seg_products = list(rewardFull[rewardFull['Segment'] == seg]['StockCode'].unique())
    seg_products = sample(seg_products, nproducts)
    return seg_products

# This is the function to get the top n products based on value
def sortlist(nproducts, stateId,seg,mypol):
    # Get the top products based on the values and sort them from product with largest value to least
    topProducts = sorted(mypol[stateId].keys(), key=lambda kv: mypol[stateId][kv])[-nproducts:][::-1]
    # If the topProducts is less than the required number of products nproducts, sample the delta
    while len(topProducts) < nproducts:
        print("[INFO] top products less than required number of products")
        segProducts = segProduct(seg,(nproducts - len(topProducts)))
        newList = topProducts + segProducts
        # Finding unique products
        topProducts = list(OrderedDict.fromkeys(newList))
    return topProducts

# This is the function to create the number of products based on exploration and exploitation
def sampProduct(seg, nproducts, stateId, epsilon,mypol):
    # Initialise an empty list for storing the recommended products
    seg_products = []
    # Get the list of unique products for each segment
    Segment_products = list(rewardFull[rewardFull['Segment'] == seg]['StockCode'].unique())
    # Get the list of top n products based on value
    topProducts = sortlist(nproducts, stateId,seg,mypol)
    # Start a loop to get the required number of products
    while len(seg_products) < nproducts:
        # First find a probability
        probability = np.random.rand()
        if probability >= epsilon:            
            # The top product would be first product in the list
            prod = topProducts[0]
            # Append the selected product to the list
            seg_products.append(prod)
            # Remove the top product once appended
            topProducts.pop(0)
            # Ensure that seg_products is unique
            seg_products = list(OrderedDict.fromkeys(seg_products))
        else:
            # If the probability is less than epsilon value randomly sample one product
            prod = sample(Segment_products, 1)[0]
            seg_products.append(prod)
            # Ensure that seg_products is unique
            seg_products = list(OrderedDict.fromkeys(seg_products))
    return seg_products

In line 117 we define the function to get the recommended products. The input parameters for the function are the segment, number of products we want to recommend, state id,epsilon value and the policy dictionary . We initialise a list to store the recommended products in line 119 and then extract all the products relevant for the segment in line 121. We then sort the segment products according to frequency of the products. We use the function ‘sortlist‘ in line 104 for this purpose. We sort the value dictionary according to the frequency and then select the top n products in the descending order in line 106. Now if the number of products in the dictionary is less than the number of products we want to be recommended, we randomly select the remaining products from the list of products for the segment. Lines 99-100 in the function ‘segproducts‘ is where we take the list of unique products for the segment and then randomly sample the required number of products and return it in line 110. In line 111 the additional products along with the top products is joined together. The new list of top products are sorted as per the order in which the products were added in line 112 and returned to the calling function in line 123.

Lines 125-142 implements the epsilon greedy process for product recommendation. This is a loop which continues till we get the required number of products for recommending. In line 127 a random probability score is generated and is verified whether it is greater than the epsilon value in line 128. If the random probability score is greater than epsilon value, we extract the topmost product from the list of products in line 130 and then append it to the recommendation candidate product list in line 132. After extraction of the top product, it is removed from the list in line 134. The list is sorted according to the order in which products are added in line 136. This loop continues till we get the required number of products for recommendation.

Lines 137-142 is the loop when the random score is less than the epsilon value i.e exploration stage. In this stage we randomly sample products from the list of products appealing to the segment and append it to the list of recommendation candidates. The final list of candiate products to be recommended is returned in line 143.

Process 3 : Updation of all relevant dictionaries

In the last section we saw the process of selecting the products for recommendation. The next process we will cover is how the products recommended are updated in the relevant dictionaries like quantity dictionary, value dictionary, reward dictionary and recommendation dictionary. Again we will use a function to update the dictionaries. The first function we will see is the one used to update sampled products.

def dicUpdater(prodList, stateId,countDic,recoCountdic,polDic,rewDic):
    # Loop through each of the products
    for prod in prodList:        
        # Check if the product is in the dictionary
        if prod in list(countDic[stateId].keys()):
            # Update the count by 1
            countDic[stateId][prod] += 1            
        else:
            countDic[stateId][prod] = 1            
        if prod in list(recoCountdic[stateId].keys()):            
            # Update the recommended products with 1
            recoCountdic[stateId][prod] += 1           
        else:
            # Initialise the recommended products as 1
            recoCountdic[stateId][prod] = 1            
        if prod not in list(polDic[stateId].keys()):
            # Initialise the value as 0
            polDic[stateId][prod] = 0            
        if prod not in list(rewDic[stateId].keys()):            
            # Initialise the reward dictionary as 0
            rewDic[stateId][prod] = GaussianDistribution(loc=0, scale=1, size=1)[0].round(2)     
            
    # Return all the dictionaries after update
    return countDic,recoCountdic,polDic,rewDic

The inputs for the function are the recommended products ,prodList , stateID, count dictionary, recommendation dictionary, value dictionary and reward dictionary as shown in line 144.

A inner loop is executed in lines 146-166, to go through each product in the product list. In line 148 a check is made to find out if the product is in the count dictionary. This entails, understanding if the product was ever bought under that state. If the product was ever bought before, the count is updated by 1. However if the product was not bought earlier, then the dictionary for that product under that state is initialised as 1 in line 152.

The next step is for updating the recommendation count for the same product. The same logic as above applies. If the product was recommended before, for that state, the number is updated by 1 if not the number is initialised to 1 in lines 153-158.

The next task is to verify if there is a value distribution for this product for the specific state as in lines 159-161. If the value distribution does not exist, it is initialised to zero. However we dont do any updation to the value distribution here. The updation to value distribution happens later on. We will come to that in a moment

The last check is to verify if the product exists in the reward dictionary for that state in lines 162-164. If it dosent exist then it is initialised with a gaussian distribution. Again we dont do any updation for reward as this is done later on.

Now that we have seen the function for updating the dictionaries, we will get into a function which initializes dictionaries. This process is required, if a particular state has never been seen for any of the dictionaries. Let us get to that function

def dicAdder(prodList, stateId,countDic,recoCountdic,polDic,rewDic):
    countDic[stateId] = {}
    polDic[stateId] = {}
    recoCountdic[stateId] = {}
    rewDic[stateId] = {}    
    # Loop through the product list
    for prod in prodList:
        # Initialise the count as 1
        countDic[stateId][prod] = 1
        # Initialise the value as 0
        polDic[stateId][prod] = 0
        # Initialise the recommended products as 1
        recoCountdic[stateId][prod] = 1
        # Initialise the reward dictionary as 0
        rewDic[stateId][prod] = GaussianDistribution(loc=0, scale=1, size=1)[0].round(2)
    # Return all the dictionaries after update
    return countDic,recoCountdic,polDic,rewDic

The inputs to this function as seen in line 168 are the same as what we saw in the update function. In lines 169-172, we initialise the innner dictionaries for the current state. Lines 174-182, all the inner dictionaries are initialised for the respective products. The count and recommendation dictionaries are initialised by 1 and the value dictionary is intialised as 0. The reward dictionary is initialised using a gaussian distribution. Finally the updated dictionaries are returned in line 184.

Next we start the recommendation process using all the functions we have defined so far.

nProducts = 10
epsilon=0.1

# Get the list of recommended products and update the dictionaries.The process is executed for a scenario when the context exists and does not exist
if len(mypolDic) > 0:    
    print("The context exists")
    # Implement the sampling of products based on exploration and exploitation
    seg_products = sampProduct(seg, nProducts , stateId, epsilon,mypol)
    # Update the dictionaries of values and rewards
    countDic,recoCountdic,polDic,rewDic = dicUpdater(seg_products, stateId,countDic,recoCountdic,polDic,rewDic)
else:
    print("The context dosent exist")
    # Get the list of relavant products
    seg_products = segProduct(seg, nProducts)
    # Add products to the value dictionary and rewards dictionary
    countDic,recoCountdic,polDic,rewDic = dicAdder(seg_products, stateId,countDic,recoCountdic,polDic,rewDic)

We define the number of products and epsilon values in lines 185-186. In line 189 we check if the state exists which would mean that there would be some products in the dictionary. If the state exists, then we get the list of recommended products using the ‘sampProducts‘ function we saw earlier in line 192. After getting the list of products we update all the dictionaries in line 194.

If the state dosent exist, then products are randomly sampled using the ‘segProduct‘ function in line 198. As before we update the dictionaries in line 200.

Process 4 : Customer Action

So far we have implemented the recommendation process. In real world application, the products we generated are displayed as recommendations to the customer. Based on the recommendations received, the customer carries out different actions as below.

  1. Customer could buy one or more of the recommended products
  2. Customer could browse through the recommended products
  3. Customer could ignore all the recommendations.

Based on the customer actions, we need to give feed back to the online learning system as to how good the recommendations were. Obviously the first scenario is the most desired one, the second one indicates some level of interest and the last one is the undesirable effect. From an self learning perspective we need to reinforce the desirable behaviours and discourage undesirable behavrious by devising proper rewards systems.

Just like we simulated customer states, we will create some functions to simulate customer actions. We define probability distribution to simulate customers propensity for buying a product or clicking a product. Based on the probability distribution we get how many products get bought or how many get clicked. Based on these numbers we sample products from our recommended list as to how many of them are going to be bought or how many would be clicked. Please note that these processes are only required as we are not implementing on a real system. When we are implementing this process in a real system, we get all these feedbacks from the the choices made by the customer.

def custAction(segproducts):
    print('[INFO] getting the customer action')
    # Sample a value to get how many products will be clicked    
    click_number = np.random.choice(np.arange(0, 10), p=[0.50,0.35,0.10, 0.025, 0.015,0.0055, 0.002,0.00125,0.00124,0.00001])
    # Sample products which will be clicked based on click number
    click_list = sample(segproducts,click_number)

    # Sample for buy values    
    buy_number = np.random.choice(np.arange(0, 10), p=[0.70,0.15,0.10, 0.025, 0.015,0.0055, 0.002,0.00125,0.00124,0.00001])
    # Sample products which will be bought based on buy number
    buy_list = sample(segproducts,buy_number)

    return click_list,buy_list

The input to the function is the recommended products as seen from line 201. We then simulate the number of products the customer is going to click using a probability distribution shown in line 204. From the probability distribution we can see there is 50% of chance for not clicking any product, 35% chance to click one product and so on. Once we get the number of products which are likely to be clicked, we sample that many products from the recommended product list. We do a similar process for products that are likely to be bought in lines 209-211. Finally we return the list of products that will be clicked and bought. Please note that there is high likelihood that the returned lists will be empty as the probability distributions are skewed heavily towards that possiblity. Let us implement that function and see what we get.

click_list,buy_list = custAction(seg_products)
print(click_list)
print(buy_list)

So from the simulation, we can see that the customer browsed one product however did not buy any of the products. Please note that you might get a very different simulation when you try as this is a random sampling of products.

Now that we have got the customer action, our next step is to get rewards based on the customer actions. As reward let us define that we will give 5 points if the customer has bought a product and a reward of 1 if the customer has clicked the product and -2 reward if the customer has done neither of these.We will define some functions to update the value dictionaries based on the rewards.

def getReward(loc):
    rew = GaussianDistribution(loc=loc, scale=1, size=1)[0].round(2)
    return rew

def saPolicy(rew, stateId, prod,polDic,recoCountdic):
    # This function gets the relavant algorithm for the policy update
    # Get the current value of the state    
    vcur = polDic[stateId][prod]    
    # Get the counts of the current product
    n = recoCountdic[stateId][prod]    
    # Calculate the new value
    Incvcur = (1 / n) * (rew - vcur)    
    return Incvcur

def valueUpdater(seg_products, loc,custList,stateId,rewDic,polDic,recoCountdic, remove=True):
    for prod in custList:       
        # Get the reward for the bought product. The reward will be centered around the defined reward for each action
        rew = getReward(loc)        
        # Update the reward in the reward dictionary
        rewDic[stateId][prod] += rew        
        # Update the policy based on the reward
        Incvcur = saPolicy(rew, stateId, prod,polDic,recoCountdic)        
        polDic[stateId][prod] += Incvcur        
        # Remove the bought product from the product list
        if remove:
            seg_products.remove(prod)
    return seg_products,rewDic,polDic,recoCountdic

The main function is in line 231, whose inputs are the following,

seg_products : segment products we earlier derived

loc : reward for action ( i.e 5 for buy, 1 for browse and -2 for ignoring)

custList : The list of products which are clicked or bought by the customer

stateId : The state ID

rewDic,polDic,recoCountdic : Reward dictionary, value dictionary and recommendation count dictionary for updates

An iterative loop is initiated from line 232 to iterate through all the products in the corresponding list ( buy or click list). First we get the corresponding reward for the action in line 234. This line calls a function defined in line 217, which returns the reward from a Gaussian distribution centred at the reward location ( 5, 1 or -2). Once we get the reward we update the reward dictionary in line 236 with the new reward.

In line 238 we call the function ‘saPolicy‘ for getting the new value for the action. The function ‘saPolicy‘ defined in line 221, takes the reward, state Id , product and dictionaries as input and output the new values for updating the policy dictionary.

In line 224, we get the current value for the state and the product and in line 226 we get the number of times that product was ever selected. The new value is calculated in line 228 through the simple averaging method we dealt with in our post on K armed bandits. The new value is then returned to the calling function and then incremented with the existing value in lines 238-239. To avoid re-recommending the current product for the customer we do a check in line 241 and then remove it from the segment products in line 242. The updated list of segment products along with the updated dictionaries are then returned in line 243.

Let us now look at the implementation of these functions next.

if len(buy_list) > 0:
    seg_products,rewDic,polDic,recoCountdic = valueUpdater(seg_products, 5, buy_list,stateId,rewDic,polDic,recoCountdic)
    # Repeat the same process for customer click
if len(click_list) > 0:
    seg_products,rewDic,polDic,recoCountdic = valueUpdater(seg_products, 1, click_list,stateId,rewDic,polDic,recoCountdic)
    # For those products not clicked or bought, give a penalty
if len(seg_products) > 0:
    custList = seg_products.copy()
    seg_products,rewDic,polDic,recoCountdic = valueUpdater(seg_products, -2, custList,stateId ,rewDic,polDic,recoCountdic, False)

In lines 245,248 and 252 we update the values for the buy list, click list and the ignored products respectively. In the process all the dictionaries also get updated.

That takes us to the end of all the processes for the self learning system. When implementing these processes as system, we have to keep implementing these processes one by one. Let us summarise all the processes which needs to be repeated to build this self learning recommendation system.

  1. Identify the customer context by simulating the states. In a real life system we dont have to simulate this information as this will be available when a customer logs in
  2. Initialise the dictionaries for the state id we generated
  3. Get the list of products to be recommended based on the state id
  4. Update the dictionaries based on the list of products which were recommended
  5. Simulate customer actions on the recommended products. Again in real systems we done simulate customer actions as it will be captured online.
  6. Update the value dictionary and reward dictionary based on customer actions.

All these 6 steps will have to be repeated for each customer instance. Once this cycle runs for some continuous steps, we will get the value dictionaries updated and dynamically aligned to individual customer segments.

What next ?

In this post we built our self learning recommendation system using Jupyter notebooks. Next we will productionise these processes using python scripts. When we productionise these processes, we will also use Mongo DB database to store and retrieve data. We will start the productionising phase in the next post.

Please subscribe to this blog post to get notifications when the next post is published.

You can also subscribe to our Youtube channel for all the videos related to this series.

The complete code base for the series is in the Bayesian Quest Git hub repository

Do you want to Climb the Machine Learning Knowledge Pyramid ?

Knowledge acquisition is such a liberating experience. The more you invest in your knowledge enhacement, the more empowered you become. The best way to acquire knowledge is by practical application or learn by doing. If you are inspired by the prospect of being empowerd by practical knowledge in Machine learning, subscribe to our Youtube channel

I would also recommend two books I have co-authored. The first one is specialised in deep learning with practical hands on exercises and interactive video and audio aids for learning

This book is accessible using the following links

The Deep Learning Workshop on Amazon

The Deep Learning Workshop on Packt

The second book equips you with practical machine learning skill sets. The pedagogy is through practical interactive exercises and activities.

The Data Science Workshop Book

This book can be accessed using the following links

The Data Science Workshop on Amazon

The Data Science Workshop on Packt

Enjoy your learning experience and be empowered !!!!

Building Self Learning Recommendation system – IV : Prototype Phase I: Segmenting the customers.

This is the fourth post of our series on building a self learning recommendation system using reinforcement learning. In the coming posts of the series we will expand on our understanding of the reinforcement learning problem and build an application for recommending products. These are the different posts of the series where we will progressively build our recommendation system.

  1. Recommendation system and reinforcement learning primer
  2. Introduction to multi armed bandit problem
  3. Self learning recommendation system as a K-armed bandit
  4. Build the prototype of the self learning recommendation system: Part I ( This post )
  5. Build the prototype of the self learning recommendation system: Part II
  6. Productionising the self learning recommendation system: Part I – Customer Segmentation
  7. Productionising the self learning recommendation system: Part II – Implementing self learning recommendation
  8. Evaluating different deployment options for the self learning recommendation systems.

Introduction

In the last post of the series we formulated the idea on how we can build the self learning recommendation system as a K armed bandit. In this post we will go ahead and start building the prototype of our self learning system based on the idea we developed. We will be using Jupyter notebook to build our prototype. Let us dive in

Processes for building our self learning recommendation system

Let us take a birds eye view of the recommendation system we are going to build. We will implement the following processes

  1. Cleaning of the data set
  2. Segment the customers using RFM segmentation
  3. Creation of states for contextual recommendation
  4. Creation of reward and value distributions
  5. Implement the self learning process using simple averaging method
  6. Simulate customer actions to initiate self learning for recommendations

The first two processes will be implemented in this post and the remaining processes will be covered in the next post.

Cleaning the data set

The data set which we would be using for this exercise would be the online retail data set. Let us load the data set in our system and get familiar with the data. First let us import all the necessary library files

from pickle import load
from pickle import dump
import numpy as np
import pandas as pd
from dateutil.parser import parse
import os
from collections import Counter
import operator
from random import sample

We will now define a simple function to load the data using pandas.

def dataLoader(orderPath):
    # THis is the method to load data from the input files    
    orders = pd.read_csv(orderPath,encoding = "ISO-8859-1")
    return orders

The above function reads the csv file and returns the data frame. Let us use this function to load the data and view the head of the data

# Please define your specific path where the data set is loaded
filename = "OnlineRetail.csv"
# Let us load the customer Details
custDetails = dataLoader(filename)
custDetails.head()
Figure 1 : Retail data set

Further in the exercise we have to work a lot with the dates and therefore we need to extract relevant details from the date column like the day, weekday, month, year etc. We will do that with the date parser library. Let us now parse all the date related column and create new columns storing the new details we extract after parsing the dates.

#Parsing  the date
custDetails['Parse_date'] = custDetails["InvoiceDate"].apply(lambda x: parse(x))
# Parsing the weekdaty
custDetails['Weekday'] = custDetails['Parse_date'].apply(lambda x: x.weekday())
# Parsing the Day
custDetails['Day'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%A"))
# Parsing the Month
custDetails['Month'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%B"))
# Extracting the year
custDetails['Year'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%Y"))
# Combining year and month together as one feature
custDetails['year_month'] = custDetails['Year'] + "_" +custDetails['Month']

custDetails.head()
Figure 2 : Data frame after date parsing

As seen from line 22 we have used the lambda() function to first parse the ‘date’ column. The parsed date is stored in a new column called ‘Parse_date’. After parsing the dates first, we carry out different operations, again using the lambda() function on the parsed date. The different operations we carry out are

  1. Extract weekday and store it in a new column called ‘Weekday’ : line 24
  2. Extract the day of the week and store it in the column ‘Day’ : line 26
  3. Extract the month and store in the column ‘Month’ : line 28
  4. Extract year and store in the column ‘Year’ : line 30

Finally, in line 32 we combine the year and month to form a new column called ‘year_month’. This is done to enable easy filtering of data based on the combination of a year and month.

We will also create a column which gives you the gross value of each puchase. Gross value can be calculated by multiplying the quantity with unit price.

# Creating gross value column
custDetails['grossValue'] = custDetails["Quantity"] * custDetails["UnitPrice"]
custDetails.head()
Figure 3 :Customer Details Data frame

The reason we are calculating the gross value is to use it for segmentation of customers which will be dealt with in the next section. This takes us to the end of the initial preparation of the data set. Next we start creating customer segments.

Creating Customer Segments

In the last post, where we formulated the problem statement, we identified that customer segment could be one of the important components of the states. In addition to the customer segment,the other components are day of purchase and period of the month. So our next endeavour is to prepare data to create the different states we require. We will start with defining the customer segment.

There are different approaches to creating customer segments. In this post we will use the RFM analysis to create customer segments. Let us get going with creation of customer segments from our data set. We will continue on the same notebook we were using so far.

import lifetimes

In line 39,We import the lifetimes package to create the RFM data from our transactional dataset. Next we will use the package to convert the transaction data to the specific format.

# Converting data to RFM format
RfmAgeTrain = lifetimes.utils.summary_data_from_transaction_data(custDetails, 'CustomerID', 'Parse_date', 'grossValue')
RfmAgeTrain

The process for getting the frequency, recency and monetary value is very simple using the life time package as shown in line 42 . From the output we can see the RFM data frame formed with each customer ID as individual row. For each of the customer, the frequency and recency in days is represented along with the average monetary value for the customer. We will be using these values for creating clusters of customer segments.

Before we work further, let us clean the data frame a bit by resetting the index values as shown in line 44

RfmAgeTrain = RfmAgeTrain.reset_index()
RfmAgeTrain

What we will now do is to use recency, frequency and monetary values seperately to create clusters. We will use the K-means clustering technique to find the number of clusters required. Many parts of the code used for clustering is taken from the following post on customer segmentation.

In lines 46-47 we import the Kmeans clustering method and matplotlib library.

from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

For the purpose of getting the recency matrix let us take a subset of the data frame with only customer ID and recency value as shown in lines 48-49

user_recency = RfmAgeTrain[['CustomerID','recency']]
user_recency.head()

In any clustering problem,as you might know, one of the critical tasks is to determine the number of clusters which in the Kmeans algorithm is a parameter. We will use the well known elbow method to find the optimum number of clusters.

# Initialize a dictionary to store sum of squared error
sse = {}
recency = user_recency[['recency']]

# Loop through different cluster combinations
for k in range(1,10):
    # Fit the Kmeans model using the iterated cluster value
    kmeans = KMeans(n_clusters=k,max_iter=2000).fit(recency)
    # Store the cluster against the sum of squared error for each cluster formation   
    sse[k] = kmeans.inertia_
    
# Plotting all the clusters
plt.figure()
plt.plot(list(sse.keys()),list(sse.values()))
plt.xlabel("Number of clusters")
plt.show()
Figure 4 : Plot of number of clusters

In line 51, we initialise a dictionary to store the sum of square error for each k-means cluster and then subset the data frame ‘recency’ with only the recency values in line 52.

From line 55, we start a loop to itrate through different cluster values. For each cluster value, we fit the k-means model in line 57. We also store the sum of squared error in line 59 for each of the cluster in the dictionary we initialized.

Lines 62-65, we visualise the number of clusters against the sum of squared error, which gives and indication of the right k value to choose.

From the plot we can see that 2,3 and 4 cluster values are where the elbow tapers and one of these values can be taken as the cluster value.Let us choose 4 clusters for our purpose and then refit the data.

# let us take four clusters 
kmeans = KMeans(n_clusters=4)
# Fit the model on the recency data
kmeans.fit(user_recency[['recency']])
# Predict the clusters for each of the customer
user_recency['RecencyCluster'] = kmeans.predict(user_recency[['recency']])
user_recency

In line 67, we instantiate the KMeans class using 4 clusters. We then use the fit method on the recency values in line 69. Once the model is fit, we predict the cluster for each customer in line 71.

From the output we can see that the recency cluster is predicted against each customer ID. We will clean up this data frame a bit, by resetting the index.

user_recency.sort_values(by='recency',ascending=False).reset_index(drop=True)

From the output we can see that the data is ordered according to the clusters. Let us also look at how the clusters are mapped vis a vis the actual recency value. For doing this, we will group the data with respect to each cluster and then find the mean of the recency value, as in line 74.

user_recency.groupby('RecencyCluster')['recency'].mean().reset_index()

From the output we see the mean value of recency for each cluster. We can clearly see that there is a demarcation of the mean values with the value of the cluster. However, the mean values are not mapped in a logical (increasing or decreasing) order of the clusters. From the output we can see that cluster 3 is mapped to the smallest recency value ( 7.72). The next smallest value (115.85) is mapped to cluster 0 and so on. So there is not specific ordering to the custer and the mean value mapping. This might be a problem when we combine all the clusters for recency, frequency and monetary together to derive a combined score. So it is necessary to sort it in an ordered fashion. We will use a custom function to get the order right. Let us see the function.

# Function for ordering cluster numbers

def order_cluster(cluster_field_name,target_field_name,data,ascending):    
    # Group the data on the clusters and summarise the target field(recency/frequency/monetary) based on the mean value
    data_new = data.groupby(cluster_field_name)[target_field_name].mean().reset_index()
    # Sort the data based on the values of the target field
    data_new = data_new.sort_values(by=target_field_name,ascending=ascending).reset_index(drop=True)
    # Create a new column called index for storing the sorted index values
    data_new['index'] = data_new.index
    # Merge the summarised data onto the original data set so that the index is mapped to the cluster
    data_final = pd.merge(data,data_new[[cluster_field_name,'index']],on=cluster_field_name)
    # From the final data drop the cluster name as the index is the new cluster
    data_final = data_final.drop([cluster_field_name],axis=1)
    # Rename the index column to cluster name
    data_final = data_final.rename(columns={'index':cluster_field_name})
    return data_final

In line 77, we define the function and its inputs. Let us look at the inputs to the function

cluster_field_name : This is the field name we give to the cluster in the data set like “RecencyCluster”.

target_field_name : This is the field pertaining to our target values like ‘recency’ , ‘frequency’ and ,’monetary_values’.

data : This is the data frame containing the cluster information and target values, for eg ( user_recency)

ascending : This is a flag indicating whether the data has to be sorted in ascending order or not

Line 79, we group the data based on the cluster and summarise the data under each group to get the mean of the target variable. The idea is to sort the data frame based on the mean values in ascending order which is done in line 81. Once the data is sorted in ascending order, we form a new feature with the data frame index as its values, in line 83. Now the index values will act as sorted cluster values and we will get a mapping between the existing cluster values and the new cluster values which are sorted. In line 85, we merge the summarised data frame with the original data frame so that the new cluster values are mapped to all the values in the data frame. Once the new sorted cluster labels are mapped to the original data frame, the old cluster labels are dropped in line 87 and the column renamed in line 89

Now that we have defined the function, let us implement it and sort the data frame in a logical order in line 91.

user_recency = order_cluster('RecencyCluster','recency',user_recency,False)

Next we will summarise the new sorted data frame and check if the clusters and mapped in a logical order.

user_recency.groupby('RecencyCluster')['recency'].mean().reset_index()

From the above output we can see that the cluster numbers are mapped in a logical order of decreasing recency.
We now need to repeat the process for frequency and monetary values. For convenience we will wrap all these processes in a new function.

def clusterSorter(target_field_name,ascending):    
    # Make the subset data frame using the required feature
    user_variable = RfmAgeTrain[['CustomerID',target_field_name]]
    # let us take four clusters indicating 4 quadrants
    kmeans = KMeans(n_clusters=4)
    kmeans.fit(user_variable[[target_field_name]])
    # Create the cluster field name from the target field name
    cluster_field_name = target_field_name + 'Cluster'
    # Create the clusters
    user_variable[cluster_field_name] = kmeans.predict(user_variable[[target_field_name]])
    # Sort and reset index
    user_variable.sort_values(by=target_field_name,ascending=ascending).reset_index(drop=True)
    # Sort the data frame according to cluster values
    user_variable = order_cluster(cluster_field_name,target_field_name,user_variable,ascending)
    return user_variable

Let us now implement this function to get the clusters for frequency and monetary values.

# Implementing for user frequency
user_freqency = clusterSorter('frequency',True)
user_freqency.groupby('frequencyCluster')['frequency'].mean().reset_index()
# Implementing for monetary values
user_monetary = clusterSorter('monetary_value',True)
user_monetary.groupby('monetary_valueCluster')['monetary_value'].mean().reset_index()

Let us now sit back and look at the three results which we got and try to analyse the results. For recency, we implemented the process using ‘ascending’ value as ‘False’ and the other two with ascending value as ‘True’. Why do you think we did it this way ?

To answer let us look these three variables from the perspective of the desirable behaviour from a customer. We would want customers who are very recent, are very frequent and spent lot of money. So from a recency perspective lesser days is a good behaviour as this indicate very recent customers. The reverse is true for frequency and monetary where the more of those values is the desirable behaviour. This is why we used 'ascending = false' in the recency variable as the clusters would be sorted with the less frequent ( more days) for cluster ‘0’ and the mean days comes down when we go to cluster 3. So in effect we are making cluster 3 as the group of most desirable customers. The reverse applies to frequency and monetary value where we gave 'ascending = True' to make custer 3 as the group of most desirable customers.

Now that we have obtained the clusters for each of the variables seperately, its time to combine them into one data frame and then get a consolidated score which will become the segments we want.

Let us first combine each of the individual dataframes we created with the original data frame

# Merging the individual data frames with the main data frame
RfmAgeTrain = pd.merge(RfmAgeTrain,user_monetary[["CustomerID",'monetary_valueCluster']],on='CustomerID')
RfmAgeTrain = pd.merge(RfmAgeTrain,user_freqency[["CustomerID",'frequencyCluster']],on='CustomerID')
RfmAgeTrain = pd.merge(RfmAgeTrain,user_recency[["CustomerID",'RecencyCluster']],on='CustomerID')
RfmAgeTrain.head()

In lines 115-117, we combine the individual dataframes to our main dataframe. We combine them on the ‘CustomerID’ field. After combining we have a consolidated data frame with each individual cluster label mapped to each customer id as shown below

Let us now add the individual cluster labels to get a combined cluster score.

# Calculate the overall score
RfmAgeTrain['OverallScore'] = RfmAgeTrain['RecencyCluster'] + RfmAgeTrain['frequencyCluster'] + RfmAgeTrain['monetary_valueCluster']
RfmAgeTrain

Let us group the data based on the ‘OverallScore’ and find the mean values of each of our variables , recency, frequency and monetary.

RfmAgeTrain.groupby('OverallScore')['frequency','recency','monetary_value'].mean().reset_index()

From the output we can see how the distributions of the new clusters are. From the values we can see that there is some level of logical demarcation according to the cluster labels. The higher cluster labels ( 4,5 & 6) have high monetary values, high frequency levels and also mid level recency levels. The first two clusters ( 0 & 1) have lower monetary values, high recency and low levels of frequency. Another stand out cluster is cluster 3, which has the lowest monetary value, lowest frequency and the lowest recency. We can very well go with these six clusters or we can combine clusters who demonstrate similar trends/behaviours. However this assessment needs to be taken based on the number of customers we have under each of these new clusters. Let us get those numbers first.

RfmAgeTrain.groupby('OverallScore')['frequency'].count().reset_index()

From the counts, we can see that the higher scores ( 4,5,6) have very few customers relative to the other clusters. So it would make sense to combine them to one single segment. As these clusters have higher values we will make them customer segment ‘Q4’. Cluster 3 has some of the lowest relative scores and so we will make it segment ‘Q1’. We can also combine clusters 0 & 1 to a single segment as the number of customers for those two clusters are also lower and make it segment ‘Q2’. Finally cluster 2 would be segment ‘Q3’ . Lets implement these steps next.

RfmAgeTrain['Segment'] = 'Q1'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 0) ,'Segment']='Q2'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 1),'Segment']='Q2'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 2),'Segment']='Q3'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 4),'Segment']='Q4'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 5),'Segment']='Q4'
RfmAgeTrain.loc[(RfmAgeTrain.OverallScore == 6),'Segment']='Q4'

RfmAgeTrain

After allocating the clusters to the respective segments, the subsequent data frame will look as above. Let us now take the mean values of each of these segments to understand how the segment values are distributed.

RfmAgeTrain.groupby('Segment')['frequency','recency','monetary_value'].mean().reset_index()

From the output we can see that for each customer segment the monetary value and frequency values are in ascending order. The value of recency is not ordered in any fashion. However that dosent matter as all what we are interested in getting is the segmentation of the customer data into four segments. Finally let us merge the segment information to the orginal customer transaction data.

# Merging the customer details with the segment
custDetails = pd.merge(custDetails, RfmAgeTrain, on=['CustomerID'], how='left')
custDetails.head()

The above output is just part of the final dataframe. From the output we can see that the segment data is updated to the original data frame.

With that we complete the first step of our process. Let us summarise what we have achieved so far.

  • Preprocessed data to extract information required to generate states
  • Transformed data to the RFM format.
  • Clustered data with respect to recency, frequency and monetary values and then generated the composite score.
  • Derived 4 segments based on the cluster data.

Having completed the segmentation of customers, we are all set to embark on the most important processes.

What Next ?

The next step is to take the segmentation information and then construct our states and action strategies from them. This will be dealt with in the next post. Let us take a peek into the processes we will implement in the next post.

  1. Create states and actions from the customer segments we just created
  2. Initialise the value distribution and rewards distribution
  3. Build the self learning recommendaton system using the epsilon greedy method
  4. Simulate customer action to get the feed backs
  5. Update the value distribution based on customer feedback and improve recommendations

There is lot of ground which will be covered in the next post.Please subscribe to this blog post to get notifications when the next post is published.

You can also subscribe to our Youtube channel for all the videos related to this series.

The complete code base for the series is in the Bayesian Quest Git hub repository

Do you want to Climb the Machine Learning Knowledge Pyramid ?

Knowledge acquisition is such a liberating experience. The more you invest in your knowledge enhacement, the more empowered you become. The best way to acquire knowledge is by practical application or learn by doing. If you are inspired by the prospect of being empowerd by practical knowledge in Machine learning, subscribe to our Youtube channel

I would also recommend two books I have co-authored. The first one is specialised in deep learning with practical hands on exercises and interactive video and audio aids for learning

This book is accessible using the following links

The Deep Learning Workshop on Amazon

The Deep Learning Workshop on Packt

The second book equips you with practical machine learning skill sets. The pedagogy is through practical interactive exercises and activities.

The Data Science Workshop Book

This book can be accessed using the following links

The Data Science Workshop on Amazon

The Data Science Workshop on Packt

Enjoy your learning experience and be empowered !!!!

Building Self Learning Recommendation system – III : Recommendation System as a K-armed Bandit

This is the third post of our series on building a self learning recommendation system using reinforcement learning. This series consists of 8 posts where in we progressively build a self learning recommendation system.

  1. Recommendation system and reinforcement learning primer
  2. Introduction to multi armed bandit problem
  3. Self learning recommendation system as a K-armed bandit ( This post )
  4. Build the prototype of the self learning recommendation system: Part I
  5. Build the prototype of the self learning recommendation system: Part II
  6. Productionising the self learning recommendation system: Part I – Customer Segmentation
  7. Productionising the self learning recommendation system: Part II – Implementing self learning recommendation
  8. Evaluating different deployment options for the self learning recommendation systems.

Introduction

In our previous post we implemented couple of experiments with K-armed bandit. When we discussed the idea of the K-armed bandits from the context of recommendation systems, we briefly touched upon the idea that the buying behavior of a customer depends on the customers context. In this post we will take the idea of the context forward and how the context will be used to build the recommendation system using the K-armed bandit solution.

Defining the context for customer buying

When we discussed about reinforcement learning in our first post, we learned about the elements of a reinforcement learning setting like state, actions, rewards etc. Let us now identify these elements in the context of the recommendation system we are building.

State

When we discussed about reinforcement learning in the first post, we learned that when an agent interacts with the environment at each time step, the agent manifests a certain state. In the example of the robot picking trash the different states were that of high charge or low charge. However in the context of the recommendation system, what would be our states ? Let us try to derive the states from the context of a customer who makes an online purchase. What would be those influencing factors which defines the product the customer buys ? Some of these are

  • The segment the customer belongs
  • The season or time of year the purchase is made
  • The day in which purchase is made

There could be many other influencing factors other than this. For simplicity let us restrict to these factors for now. A state could be made from the combinations of all these factors. Let us arrive at these factors through some exploratory analysis of the data

The data set we would be using is the online retail data set available in the UCI Machine learning library. We will download the data and the place it in local folder and the read the file from the local folder.

import numpy as np
import pandas as pd
from dateutil.parser import parse

Lines 1-3 imports all the necessary packages for our purpose. Let us now load the data as a pandas data frame

# Please use the path to the actual data
filename = "data/Online Retail.xlsx"
# Let us load the customer Details
custDetails = pd.read_excel(filename, engine='openpyxl')
custDetails.head()
Figure 1: Head of the retail data set

In line 5, we load the data from disk and then read the excel shee using the ‘openpyxl’ engine. Please note to pip install the ‘openpyxl’ package if not available.

Let us now parse the date column using date parser and extract information from the date column.

#Parsing  the date
custDetails['Parse_date'] = custDetails["InvoiceDate"].apply(lambda x: parse(str(x)))
# Parsing the weekdaty
custDetails['Weekday'] = custDetails['Parse_date'].apply(lambda x: x.weekday())
# Parsing the Day
custDetails['Day'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%A"))
# Parsing the Month
custDetails['Month'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%B"))
# Getting the year
custDetails['Year'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%Y"))
# Getting year and month together as one feature
custDetails['year_month'] = custDetails['Year'] + "_" +custDetails['Month']
# Feature engineering of the customer details data frame
# Get the date  as a seperate column
custDetails['Date'] = custDetails['Parse_date'].apply(lambda x: x.strftime("%d"))
# Converting date to float for easy comparison
custDetails['Date']  = custDetails['Date'] .astype('float64')
# Get the period of month column
custDetails['monthPeriod'] = custDetails['Date'].apply(lambda x: int(x > 15))

custDetails.head()
Figure 2 : Parsed Data

As seen from line 11 we have used the lambda() function to first parse the ‘date’ column. The parsed date is stored in a new column called ‘Parse_date’. After parsing the dates first, we carry out different operations, again using the lambda() function on the parsed date. The different operations we carry out are

  1. Extract weekday and store it in a new column called ‘Weekday’ : line 13
  2. Extract the day of the week and store it in the column ‘Day’ : line 15
  3. Extract the month and store in the column ‘Month’ : line 17
  4. Extract year and store in the column ‘Year’ : line 19

In line 21 we combine the year and month to form a new column called ‘year_month’. This is done to enable easy filtering of data, based on the combination of a year and month.

We make some more changes from line 24-28. In line 24, we extract the date of the month and then convert it into a float type in line 26. The purpose of taking the date is to find out which of these transactions have happened before 15th of the month and which after 15th. We extract those details in line 28, where we create a binary points ( 0 & 1) as to whether a date falls in the last 15 days or the first 15 days of the month.

We will also create a column which gives you the gross value of each puchase. Gross value can be calculated by multiplying the quantity with unit price. After that we will consolidate the data for each unique invoice number and then explore some of the elements of states which we want to explore

# Creating gross value column
custDetails['grossValue'] = custDetails["Quantity"] * custDetails["UnitPrice"]
# Consolidating accross the invoice number for gross value
retailConsol = custDetails.groupby('InvoiceNo')['grossValue'].sum().reset_index()
print(retailConsol.shape)
retailConsol.head()
Figure 3: Aggregated Data

Now that we have got the data consolidated based on each invoice number, let us merge the date related features from the original data frame with this consolidated data. We merge the consolidated data with the custDetails data frame and then drop all the duplicate data so that we get a record per invoice number, along with its date features.

# Merge the other information like date, week, month etc
retail = pd.merge(retailConsol,custDetails[["InvoiceNo",'Parse_date','Weekday','Day','Month','Year','year_month','monthPeriod']],how='left',on='InvoiceNo')
# dropping ALL duplicate values
retail.drop_duplicates(subset ="InvoiceNo",keep = 'first', inplace = True)
print(retail.shape)
retail.head()
Figure 4 : Consolidated data

Let us first look at the month wise consolidation of data and then plot the data. We will use a functions to map the months to its index position. This is required to plot the data according to months. The function ‘monthMapping‘, maps an integer value to the month and then sort the data frame.

# Create a map for each month
def monthMapping(mnthTrend):
    # Get the map
    mnthMap = {"January": 1, "February": 2,"March": 3, "April": 4,"May": 5, "June": 6,"July": 7, "August": 8,"September": 9, "October": 10,"November": 11, "December": 12}
    # Create a new feature for month
    mnthTrend['mnth'] = mnthTrend.Month
    # Replace with the numerical value
    mnthTrend['mnth'] = mnthTrend['mnth'].map(mnthMap)
    # Sort the data frame according to the month value
    return mnthTrend.sort_values(by = 'mnth').reset_index()

We will use the above function to consolidate the data according to the months and then plot month wise grossvalue data

mnthTrend = retail.groupby(['Month'])['grossValue'].agg('mean').reset_index().sort_values(by = 'grossValue',ascending = False)
# sort the months in the right order
mnthTrend = monthMapping(mnthTrend)
sns.set(rc = {'figure.figsize':(20,8)})
sns.lineplot(data=mnthTrend, x='Month', y='grossValue')
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
plt.show()

We can see that there is sufficient amount of variability of data month on month. So therefore we will take months as one of the context items on which the states can be constructed.

Let us now look at buying pattern within each month and check how the buying pattern is within the first 15 days and the latter half

# Aggregating data for the first 15 days and latter 15 days
fortnighTrend = retail.groupby(['monthPeriod'])['grossValue'].agg('mean').reset_index().sort_values(by = 'grossValue',ascending = False)

sns.set(rc = {'figure.figsize':(20,8)})
sns.lineplot(data=fortnighTrend, x='monthPeriod', y='grossValue')
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
plt.show()

We can see that there is as small difference between buying patterns in the first 15 days of the month and the latter half of the month. Eventhough the difference is not significant, we will still take this difference as another context.

Next let us aggregate data as per the days of the week and and check the trend

# Aggregating data accross weekdays
dayTrend = retail.groupby(['Weekday'])['grossValue'].agg('mean').reset_index().sort_values(by = 'grossValue',ascending = False)

sns.set(rc = {'figure.figsize':(20,8)})
sns.lineplot(data=dayTrend, x='Weekday', y='grossValue')
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)
plt.show()

We can also see that there is quite a bit of variability of buying patterns accross the days of the week. We will therefore take the week days also as another context

So far we have observed 4 different features, which will become our context for recommending products. The context which we have defined would act as the states from the reinforcement learning setting perspective. Let us now look at the big picture of how we will formulate the recommendation task as reinforcement learning setting.

The Big Picture

Figure 5: The Big Picture

We will now have a look at the big picture of this implementation. The above figure is the representation of what we will implement in code in the next few posts.

The process starts with the customer context, consisting of segment, month, period in the month and day of the week. The combination of all the contexts will form the state. From an implementation perspective we will run simulations to generate the context since we do not have a real system where customers logs in and thereby we automatically capture context.

Based on the context, the system will recommend different products to the customer. From a reinforcement learning context these are the actions which are taken from each state. The initial recommendation of products ( actions taken) will be based on the value function learned from the historical data.

The customer will give rewards/feedback based on the actions taken( products recommended ). The feedback would be the manifestation of the choices the customer make. The choice the customer makes like the products the customer buys, browses and ignores from the recommended list. Depending on the choice made by the customer, a certain reward will be generated. Again from an implementation perspective, since we do not have real customers giving feedback, we will be simulating the customer feedback mechanism.

Finally the update of the value functions based on the reward generated will be done based on the simple averaging method. Based on the value update, the bandit will learn and adapt to the affinities of the customers in the long run.

What next ?

In this post we explored the data and then got a big picture of what we will implement going forward. In the next post we will start implementing these processes and building a prototype using Jupyter notebook. Later on we will build an application using Python scripts and then explore options to deploy the application. Watch out this space for more.

 The next post will be published next week. Please subscribe to this blog post to get notifications when the next post is published.

You can also subscribe to our Youtube channel for all the videos related to this series.

The complete code base for the series is in the Bayesian Quest Git hub repository

Do you want to Climb the Machine Learning Knowledge Pyramid ?

Knowledge acquisition is such a liberating experience. The more you invest in your knowledge enhacement, the more empowered you become. The best way to acquire knowledge is by practical application or learn by doing. If you are inspired by the prospect of being empowerd by practical knowledge in Machine learning, subscribe to our Youtube channel

I would also recommend two books I have co-authored. The first one is specialised in deep learning with practical hands on exercises and interactive video and audio aids for learning

This book is accessible using the following links

The Deep Learning Workshop on Amazon

The Deep Learning Workshop on Packt

The second book equips you with practical machine learning skill sets. The pedagogy is through practical interactive exercises and activities.

The Data Science Workshop Book

This book can be accessed using the following links

The Data Science Workshop on Amazon

The Data Science Workshop on Packt

Enjoy your learning experience and be empowered !!!!

Building Self Learning Recommendation system using Reinforcement Learning – II : The bandit problem

This is the second post of our series on building a self learning recommendation system using reinforcement learning. This series consists of 7 posts where in we progressively build a self learning recommendation system.

  1. Recommendation system and reinforcement learning primer
  2. Introduction to multi armed bandit problem ( This post )
  3. Self learning recommendation system as a bandit problem
  4. Build the prototype of the self learning recommendation system: Part I
  5. Build the prototype of the self learning recommendation system: Part II
  6. Productionising the self learning recommendation system: Part I – Customer Segmentation
  7. Productionising the self learning recommendation system: Part II – Implementing self learning recommendation
  8. Evaluating different deployment options for the self learning recommendation systems.

Introduction

Figure 1 : Reinforcement Learning Setting

In our previous post we introduced different types of recommendation systems and explored some of the basic elements of reinforcement learning. We found out that reinforcement learning problems evaluates different actions when the agent is in a specific state. The action taken generates a certain reward. In other words we get a feedback on how good the action was based on the reward we got. However we wont get the feed back as to whether the action taken was the best available. This is what contrasts reinforcement learning from supervised learning. In supervised learning the feed back is instructive and gives you the quantum of the correctness of an action based on the error. Since reinforcement learning is evaluative, it depends a lot on exploring different actions under different states to find the best one. This tradeoff between exploration and exploitation is the bedrock of reinforcement learning problems like the K armed bandit. Let us dive in.

The Bandit Problem.

In this section we will try to understand K armed bandit problem setting from the perspective of product recommendation.

A recommendation system recommends a set of products to a customer based on the customers buying patterns which we call as the context. The context of the customer can be the segment the customer belongs to, the period in which the customer buys, like which month, which week of the month, which day of the week etc. Once recommendations are made to a customer, the customer based on his or her affinity can take different type of actions i.e. (i) ignore the recommendation (ii) click on the product and further explore (iii) buy the recommended product. The objective of the recommendation system would be to recommend those products which are most likely to be accepted by the customer or in other words maximize the value from the recommendations.

Based on the recommendation example let us try to draw parallels to the K armed bandit. The K-armed bandit is a slot machine which has ‘K’ different arms or levers. Each pull of the lever can have a different outcome. The outcomes can vary from no payoff to winning a jackpot. Your objective is to find the best arm which yields the best payoff through repeated selection of the ‘K’ arms. This is where we can draw parallels’ between armed bandits and recommendation systems. The products recommended to a customer are like the levers of the bandit. The value realization from the recommended products happens based on whether the customer clicks on the recommended product or buys them. So the aim of the recommendation system is to identify the products which will generate the best value i.e which will very likely be bought or clicked by the customer.

Figure 2 : Recommendation system as K lever bandits

Having set the context of the problem statement , we will understand in depth the dynamics of the K-armed bandit problem and couple of solutions for solving them. This will lay the necessary foundation for us to try this in creating our recommendation system.

Non-Stationary Armed bandit problem

When we discussed about reinforcement learning we learned about the reward function. The reward function can be of two types, stationary and non-stationary. In stationary type the reward function will not change over time. So over time if we explore different levers we will be able to figure out which lever gives the best value and stick to it. In contrast,in the non stationary problem, the reward function changes over time. For non stationary problem, identifying the arms which gives the best value will be based on observing the rewards generated in the past for each of the arms. This scenario is more aligned with real life cases where we really do not know what would drive a customer at a certain point of time. However we might be able to draw a behaviour profile by observing different transactions over time. We will be exploring the non-stationary type of problem in this post.

Exploration v/s exploitation

Figure 3 : Should I exploit the current lever or explore ?

One major dilemma in problems like the bandit is the choice between exploration and exploitation. Let us explain this with our context. Let us say after few pulls of the first four levers we found that lever 3 has been consistently giving good rewards. In this scenario, a prudent strategy would be to keep on pulling the 3rd lever as we are sure that this is the best known lever. This is called exploitation. In this case we are exploiting our knowledge about the lever which gives the best reward. We also call the exploitation of the best know lever as the greedy method.

However the question is, will exploitation of our current knowledge guarantee that we get the best value in the long run ? The answer is no. This is because, so far we have only tried the first 4 levers, we haven’t tried the other levers from 5 to 10. What if there was another lever which is capable of giving higher reward ? How will we identify those unknown high value levers if we keep sticking to our known best lever ? This dilemma is called the exploitation v/s exploration. Having said that, resorting to always exploring will also be not judicious. It is found out that a mix of exploitation and exploration yields the best value over a long run.

Methods which adopt a mix of exploitation and exploration are called ε greedy methods. In such methods we exploit the greedy method most of the time. However at some instances, say with a small probability of ε we randomly sample from other levers also so that we get a mix of exploitation and exploration. We will explore different ε greedy methods in the subsequent sections

Simple averaging method

In our discussions so far we have seen that the dynamics of reinforcement learning involves actions taken from different states yielding rewards based on the state-action pair chosen. The ultimate aim is to maximize the rewards in the long run. In order to maximize the overall rewards, it is required to exploit the actions which gets you the maximum rewards in the long run. However to identify the actions with the highest potential we need to estimate the value of that action over time. Let us first explore one of the methods called simple averaging method.

Let us denote the value of an action (a) at time t as Qt(a). Using simple averaging method Qt(a) can be estimated by summing up all the rewards received for the action (a) divided by the number of times action (a) was selected. This can be represented mathematically as

In this equation R1 .. Rn-1 represents the rewards received till time (t) for action (a)

However we know that the estimate of value are a moving average, which means that there would be further instances when action (a) will be selected and corresponding rewards received. However it would be tedious to always sum up all the rewards and then divide it by the number of instances. To avoid such tedious steps, the above equation can be rewritten as follows

This is a simple update formulae where Qn+1 is the new estimate for the n+1 occurance of action a, Qn is the estimate till the nth try and Rn is the reward received for the nth try .

In simple terms this formulae can be represented as follows

New Estimate <----- Old estimate + Step Size [ Reward - Old Estimate]

For simple averaging method the Step Size is the reciprocal of the number of times that particular action was selected ( 1/n)

Now that we have seen the estimate generation using the simple averaging method, let us look at the complete algorithm.

  1. Initialize values for the bandit arms from 1 to K. Usually we initialize a value of 0 for all the bandit arms
  2. Define matrices to store the Value estimates for all the arms ( Qt(a) ) and initialize it to zero
  3. Define matrices to store the tracker for all the arms i.e a tracker which stores the number of times each arm was pulled
  4. Start a iterative loop and
    • Sample a random probability value
    • if the probability value is greater than ε, pick the arm with the largest value. If the probability value is less than ε, randomly pick an arm.
  5. Get the reward for the selected arm
  6. Update the number tracking matrix with 1 for the arm which was selected
  7. Update the Qt(a) matrix, for the arm which was picked using the simple averaging formulae.

Let us look at python implementation of the simple averaging problem next

Implementation of Simple averaging method for K armed bandit

In this implementation we will experiment with around 2000 different bandits with each bandit having 10 arms each. We will be evaluating these bandits for around 10000 steps. Finally we will average the values across all the bandits for each time step. Let us dive into the implementation.

Let us first import all the required packages for the implementation in lines 1-4

import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from numpy.random import normal as GaussianDistribution

We will start off by defining all the parameters of our bandit implementation. We would have 2000 seperate bandit experiments. Each bandit experiment will run for around 10000 steps. As defined earlier each bandit will have 10 arms. Let us now first define these parameters

# Define the armed bandit variables
nB = 2000 # Number of bandits
nS = 10000 # Number of steps we will take for each bandit
nA = 10 # Number of arms or actions of the bandit
nT = 2 # Number of solutions we would apply

As we discussed in the previous post the way we arrive at the most optimal policy is through the rewards an agent receives in the process of interacting with the environment. The policy defines the actions the agent will take. In our case, the actions we are going to take is the arms which we are going to pull. The reward which we get from our actions is based on the internal calibration of the armed bandit. The policy we will adopt is a mix of exploitation and exploration. This means that most of the time we will exploit the which action which was found to give the best reward. However once in a while we also do a bit of exploration. The exploration is controlled by a parameter ε.

Next let us define the containers to store the rewards which we get from each arm and also to track whether the reward we got was the most optimal reward.

# Defining the rewards container
rewards = np.full((nT, nB, nS), fill_value=0.)
# Defining the optimal selection container
optimal_selections = np.full((nT, nB, nS), fill_value=0.)
print('Rewards tracker shape',rewards.shape)
print('Optimal reward tracker shape',optimal_selections.shape)

We saw earlier that the policy with which we would pull each arm would be a mixture of exploitation and exploration. The way we do exploitation is by looking at the average reward obtained from each arm and then selecting the arm which has the maximum reward. For tracking the rewards obtained from each arm we initialize some values for each of the arm and then store the rewards we receive after each pull of the arm.

To start off we initialize all these values as zero as we don’t have any information about the arms and its reward possibilities.

# Set the initial values of our actions
action_Mental_model = np.full(nA, fill_value=0.0) # action_value_estimates > action_Mental_model
print(action_Mental_model.shape)
action_Mental_model

The rewards generated by each arm of the bandit is through the internal calibration of the bandit. Let us also define how that calibration has to be. For this case we will assume that the internal calibration follows a non stationary process. This means that with each pull of the armed bandit the existing value of the armed bandit is incremented by a small value. The value to increment the internal value of the armed bandits is through a Gaussian process with its mean at 0 and a standard deviation of 1.

As a start we will initialize the calibrated values of the bandit to be zero.

# Initialize the bandit calibration values
arm_caliberated_value = np.full(nA, fill_value=0.0) 
arm_caliberated_value

We also need to track how many times a particular action was selected. Therefore we define a counter to store those values.

# Initialize the count of how many times an action was selected
arm_selected_count = np.full(nA, fill_value=0, dtype="int64") 
arm_selected_count

The last of the parameters we will define is the exploration probability value. This value defines how often we would be exploring non greedy arms to find their potential.

# Define the epsilon (ε) value 
epsilon=0.1

Now we are ready to start our experiments. The first step in the process is to decide whether we want to do exploration or exploitation. To decide this , we randomly sample a value between 0 and 1 and compare it with the exploration probability value ( ε) value we selected. If the sampled value is less than the epsilon value, we will explore, otherwise we will exploit. To explore we randomly choose one of the 10 actions or bandit arms irrespective of the value we know it has. If the random probability value is greater than the epsilon value we go into the exploitation zone. For exploitation we pick the arm which we know generates the maximum reward.

# First determine whether we need to explore or exploit
probability = np.random.rand()
probability

The value which we got is greater than the epsilon value and therefore we will resort to exploitation. If the value were to be less than 0.1 (epsilon value : ε ) we would have explored different arms. Please note that the probability value you will get will be different as this is a random generation process.

Now,let us define a decision mechanism so as to give us the arm which needs to be pulled ( our action) based on the probabiliy value.

# Our decision mechanism
if probability >= epsilon:
  my_action = np.argmax(action_Mental_model)
else:
  my_action = np.random.choice(nA)
print('Selected Action',my_action)

In the above section, in line 31 we check whether the probability we generated is greater than the epsilon value . if It it is greater, we exploit our knowledge about the value of the arms and select the arm which has so far provided the greatest reward ( line 33 ). If the value is less than the epsilon value, we resort to exploration wherein we randomly select an arm as shown in line 35. We can see that the action selected is the first action ( index 0) as we are still in the initial values.

Once we have selected our action (arm) ,we have to determine whether the arm is the best arm in terms of the reward potential in comparison with other arms of the bandit. To do that, we find the arm of the bandit which provides the greatest reward. We do this by taking the argmax of all the values of the bandit as in line 38.

# Find the most optimal arm of the bandits based on its internal calibration calculations
optimal_calibrated_arm = np.argmax(arm_caliberated_value)
optimal_calibrated_arm

Having found the best arm its now time to determine if the value which we as the user have received is equal to the most optimal value of the bandit. The most optimal value of the bandit is the value corresponding to the best arm. We do that in line 40.

# Find the value corresponding to the most optimal calibrated arm
optimal_calibrated_value = arm_caliberated_value[optimal_calibrated_arm]

Now we check if the maximum value of the bandit is equal to the value the user has received. If both are equal then the user has made the most optimal pull, otherwise the pull is not optimal as represented in line 42.

# Check whether the value corresponding to action selected by the user and the internal optimal action value are same.
optimal_pull = float(optimal_calibrated_value == arm_caliberated_value[my_action])
optimal_pull

As we are still on the initial values we know that both values are the same and therefore the pull is optimal as represented by the boolean value 1.0 for optimal pull.

Now that we have made the most optimal pull, we also need to get rewards conssumerate with our action. Let us assume that the rewards are generated from the armed bandit using a gaussian process centered on the value of the arm which the user has pulled.

# Calculate the reward which is a random distribution centered at the selected action value
reward = GaussianDistribution(loc=arm_caliberated_value[my_action], scale=1, size=1)[0]
reward

1.52

In line 45 we generate rewards using a Gaussian distribution with its mean value as the value of the arm the user has pulled. In this example we get a value of around 1.52 which we will further store as the reward we have received. Please note that since this is a random generation process, the values you would get could be different from this value.

Next we will keep track of the arms we pulled in the current experiment.

# Update the arm selected count by 1
arm_selected_count[my_action] += 1
arm_selected_count

Since the optimal arm was the first arm, we update the count of the first arm as 1 as shown in the output.

Next we are going to update our estimated value of each of the arms we select. The values we will be updating will be a function of the reward we get and also the current value it already has. So if the current value is Vcur, then the new value to be updated will be Vcur + (1/n) * (r - Vcur) where n is the number of times we have visited that particular arm and 'r' the reward we have got for pulling that arm.

To calcualte this updated value we need to first find the following values

Vcur and n . Let us get those values first

Vcur would be estimated value corresponding to the arm we have just pulled

# Get the current value of our action
Vcur = action_Mental_model[my_action]
Vcur

0.0

n would be the number of times the current arm was pulled

# Get the count of the number of times the arm was exploited
n = arm_selected_count[my_action]
n

1

Now we will update the new value against the estimates of the arms we are tracking.

# Update the new value for the selected action
action_Mental_model[my_action] = Vcur + (1/n) * (reward - Vcur)
action_Mental_model

As seen from the output the current value of the arm we pulled is updated in the tracker. With each successive pull of the arm, we will keep updating the reward estimates. After updating the value generated from each pull the next task we have to do is to update the internal calibration of the armed bandit as we are dealing with a non stationary value function.

# Increment the calibration value based on a Gaussian distribution
increment = GaussianDistribution(loc=0, scale=0.01, size=nA)
# Update the arm values with the updated value
arm_caliberated_value += increment
# Updated arm value
arm_caliberated_value

As seen from lines 59-64, we first generate a small incremental value from a Gaussian distribution with mean 0 and standard deviation 0.01. We add this value to the current value of the internal calibration of the arm to get the new value. Please note that you will get a different value for these processes as this is a random generation of values.

These are the set of processes for one iteration of a bandit. We will continue these iterations for 2000 bandits and for each bandit we will iterate for 10000 steps. In order to run these processes for all the iterations, it is better to represent many of the processes as separate functions and then iterate it through. Let us get going with that task.

Function 1 : Function to select actions

The first of the functions is the one to generate the actions we are going to take.

def Myaction(epsilon,action_Mental_model):
    probability = np.random.rand()
    if probability >= epsilon:
        return np.argmax(action_Mental_model)

    return np.random.choice(nA)

Function 2 : Function to check whether action is optimal and generate rewards

The next function is to check whether our action is the most optimal one and generate the reward for our action.

def Optimalaction_reward(my_action,arm_caliberated_value):
  # Find the most optimal arm of the bandits based on its internal calibration calculations
  optimal_calibrated_arm = np.argmax(arm_caliberated_value)
  # Then find the value corresponding to the most optimal calibrated arm
  optimal_calibrated_value = arm_caliberated_value[optimal_calibrated_arm]
  # Check whether the value of the test bed corresponding to action selected by the user and the internal optimal action value of the test bed are same.
  optimal_pull = float(optimal_calibrated_value == arm_caliberated_value[my_action])
  # Calculate the reward which is a random distribution centred at the selected action value
  reward = GaussianDistribution(loc=arm_caliberated_value[my_action], scale=1, size=1)[0]
  return optimal_pull,reward

Function 3 : Function to update the estimated value of arms of the bandit

def updateMental_model(my_action, reward,arm_selected_count,action_Mental_model):
  # Update the arm selected count with the latest count
  arm_selected_count[my_action] += 1
  # find the current value of the arm selected
  Vcur = action_Mental_model[my_action]
  # Find the number of times the arm was pulled
  n = arm_selected_count[my_action]
  # Update the value of the current arm 
  action_Mental_model[my_action] = Vcur + (1/n) * (reward - Vcur)
  # Return the arm selected and our mental model
  return arm_selected_count,action_Mental_model

Function 4 : Function to increment reward values of the bandits

The last of the functions is the function we use to make the reward generation non-stationary.

def calibrateArm(arm_caliberated_value):
    increment = GaussianDistribution(loc=0, scale=0.01, size=nA)
    arm_caliberated_value += increment
    return arm_caliberated_value

Now that we have defined the functions, we will use these functions to iterate through different bandits and multiple steps for each bandit.

for nB_i in tqdm(range(nB)):
  # Initialize the calibration values for the bandits
  arm_caliberated_value = np.full(nA, fill_value=0.0)
  # Set the initial values of the mental model for each bandit
  action_Mental_model = np.full(nA, fill_value=0.0)
  # Initialize the count of how many times an arm was selected
  arm_selected_count = np.full(nA, fill_value=0, dtype="int64")
  # Define the epsilon value for probability of exploration
  epsilon=0.1
  for nS_i in range(nS):
    # First select an action using the helper function
    my_action = Myaction(epsilon,action_Mental_model)
    # Check whether the action is optimal and calculate the reward
    optimal_pull,reward = Optimalaction_reward(my_action,arm_caliberated_value)
    # Update the mental model estimates with the latest action selected and also the reward received
    arm_selected_count,action_Mental_model = updateMental_model(my_action, reward,arm_selected_count,action_Mental_model)
    # store the rewards
    rewards[0][nB_i][nS_i] = reward
    # Update the optimal step selection counter
    optimal_selections[0][nB_i][nS_i] = optimal_pull
    # Recalibrate the bandit values
    arm_caliberated_value = calibrateArm(arm_caliberated_value)

In line 96, we start the first iterative loop to iterate through each of the set of bandits . Lines 98-104, we initialize the value trackers of the bandit and also the rewards we receive from the bandits. Finally we also define the epsilon value. From lines 105-117, we carry out many of the processes we mentioned earlier like

  • Selecting our action i.e the arm we would be pulling ( line 107)
  • Validating whether our action is optimal or not and getting the rewards for our action ( line 109)
  • Updating the count of our actions and updating the rewards for the actions ( line 111 )
  • Store the rewards and optimal action counts ( lines 113-115)
  • Incrementing the internal value of the bandit ( line 117)

Let us now run the processes and capture the values.

Let us now average the rewards which we have got accross the number of bandit experiments and visualise the reward trends as the number of steps increase.

# Averaging the rewards for all the bandits along the number of steps taken
avgRewards = np.average(rewards[0], axis=0)
avgRewards.shape
plt.plot(avgRewards, label='Sample weighted average')
plt.legend()
plt.xlabel("Steps")
plt.ylabel("Average reward")
plt.show()

From the plot we can see that the average value of rewards increases as the number of steps increases. This means that with increasing number of steps, we move towards optimality which is reflected in the rewards we get.

Let us now look at the estimated values of each arm and also look at how many times each of the arms were pulled.

# Average rewards received by each arm
action_Mental_model

From the average values we can see that the last arm has the highest value of 1.1065. Let us now look at the counts where these arms were pulled.

# No of times each arm was pulled
arm_selected_count

From the arm selection counts, we can see that the last arm was pulled the maximum. This indicates that as the number of steps increased our actions were aligned to the arms which gave the maximum value.

However even though the average value increased with more steps, does it mean that most of the times our actions were the most optimal ? Let us now look at how many times we selected the most optimal actions by visualizing the optimal pull counts.

# Plot of the most optimal actions 
average_run_optimality = np.average(optimal_selections[0], axis=0)
average_run_optimality.shape
plt.plot(average_run_optimality, label='Simple weighted averaging')
plt.legend()
plt.xlabel("Steps")
plt.ylabel("% Optimal action")
plt.show()

From the above plot we can see that there is an increase in the counts of optimal actions selected in the initial steps after which the counts of the optimal actions, plateau’s. And finally we can see that the optimal actions were selected only around 40% of the time. This means that even though there is an increasing trend in the reward value with number of steps, there is still room for more value to be obtained. So if we increase the proportion of the most optimal actions, there would be a commensurate increase in the average value which will be rewarded by the bandits. To achieve that we might have to tweak the way how the rewards are calculated and stored for each arm. One effective way is to use the weighted averaging method

Weighted Averaging Method

When we were dealing with the simple averaging method, we found that the update formule was as follows

New Estimate <----- Old estimate + Step Size [ Reward - Old Estimate]

In the formule, the Step Size for simple averaging method is the reciprocal of the number of times that particular action was selected ( 1/n)

In weighted averaging method we make a small variation in the step size. In this method we use a constant step size method called alpha. The new update formule would be as follows

Qn+1 = Qn + alpha * (reward - Qn)

Usually we take some small values of alpha less than 1 say 0.1 or 0.01 or values similar to that.

Let us now try the weighted averaging method with a step size of 0.1 and observe what difference this method have on the optimal values of each arm.

In the weighted averaging method all the steps are the same as the simple averaging, except for the arm update method which is a little different. Let us define the new update function.

def updateMental_model_WA(my_action, reward,action_Mental_model):
  alpha=0.1 
  qn = action_Mental_model[my_action]
  action_Mental_model[my_action] = qn + alpha * (reward - qn)
  return action_Mental_model

Let us now run the process again with the updated method. Please note that we store the values in the same rewards and optimal_selection matrices. We store the value of weighted average method in index [1]

for nB_i in tqdm(range(nB)):
  # Initialize the calibration values for the bandits
  arm_caliberated_value = np.full(nA, fill_value=0.0)
  # Set the initial values of the mental model for each bandit
  action_Mental_model = np.full(nA, fill_value=0.0)  
  # Define the epsilon value for probability of exploration
  epsilon=0.1
  for nS_i in range(nS):
    # First select an action using the helper function
    my_action = Myaction(epsilon,action_Mental_model)
    # Check whether the action is optimal and calculate the reward
    optimal_pull,reward = Optimalaction_reward(my_action,arm_caliberated_value)
    # Update the mental model estimates with the latest action selected and also the reward received
    action_Mental_model = updateMental_model_WA(my_action, reward,action_Mental_model)
    # store the rewards
    rewards[1][nB_i][nS_i] = reward
    # Update the optimal step selection counter
    optimal_selections[1][nB_i][nS_i] = optimal_pull
    # Recalibrate the bandit values
    arm_caliberated_value = calibrateArm(arm_caliberated_value)

Let us look at the plots for the weighted averaging method.

average_run_rewards = np.average(rewards[1], axis=0)
average_run_rewards.shape
plt.plot(average_run_rewards, label='weighted average')

plt.legend()
plt.xlabel("Steps")
plt.ylabel("Average reward")
plt.show()

From the plot we can see that the average reward increasing with number of steps. We can also notice that the average values obtained higher than the simple averaging method. In the simple averaging method the average value was between 1 and 1.2. However in the weighted averaging method the average value reaches within the range of 1.2 to 1.4. Let us now see how the optimal pull counts fare.

average_run_optimality = np.average(optimal_selections[1], axis=0)
average_run_optimality.shape
plt.plot(average_run_optimality, label='Weighted averaging')
plt.legend()
plt.xlabel("Steps")
plt.ylabel("% Optimal action")
plt.show()

We can observe from the above plot that we take the optimal action for almost 80% of the time as the number of steps progress towards 10000. If you remember the optimal action percentage was around 40% for the simple averaging method. The plots show that the weighted averaging method performs better than the simple averaging method.

Wrapping up

In this post we have understood two methods of finding optimal values for a K armed bandit. The solution space is not limited to these two methods and there are many more methods for solving the bandit problem. The list below are just few of them

  • Upper Confidence Bound Algorithm ( UCB )
  • Bayesian UCB Algorithm
  • Exponential weighted Algorithm
  • Softmax Algorithm

Bandit problems are very useful for many use cases like recommendation engines, website optimization, click through rate etc. We will see more use cases of bandit algorithm in some future posts

What next ?

Having understood the bandit problem, our next endeavor would be to use the concepts in building a self learning recommendation system. The next post would be a pre-cursor to that. In the next post we will formulate our problem context and define the processes for building the self learning recommendation system using a bandit algorithm. This post will be released next week ( Jan 17th 2022).

Please subscribe to this blog post to get notifications when the next post is published.

You can also subscribe to our Youtube channel for all the videos related to this series.

The complete code base for the series is in the Bayesian Quest Git hub repository

Do you want to Climb the Machine Learning Knowledge Pyramid ?

Knowledge acquisition is such a liberating experience. The more you invest in your knowledge enhacement, the more empowered you become. The best way to acquire knowledge is by practical application or learn by doing. If you are inspired by the prospect of being empowerd by practical knowledge in Machine learning, subscribe to our Youtube channel

I would also recommend two books I have co-authored. The first one is specialised in deep learning with practical hands on exercises and interactive video and audio aids for learning

This book is accessible using the following links

The Deep Learning Workshop on Amazon

The Deep Learning Workshop on Packt

The second book equips you with practical machine learning skill sets. The pedagogy is through practical interactive exercises and activities.

The Data Science Workshop Book

This book can be accessed using the following links

The Data Science Workshop on Amazon

The Data Science Workshop on Packt

Enjoy your learning experience and be empowered !!!!

.