
Ask a chatbot “which promotion ought to we run extra of,” and it solutions in a single breath. It picks a quantity, states it with confidence, and stops. It picks the promotion with the best-looking quantity and states its alternative confidently. However it might by no means test how a lot information that quantity relies on. A promotion that appears nice after 10 orders is far much less convincing than one which performs nicely throughout 1,000 orders.
A senior analyst works slower on goal. They restate the query, kind a speculation, write the question, then test whether or not the end result has sufficient information behind it earlier than they are saying something to an government.
We will construct that self-discipline into code.
On this walkthrough, we construct a small Python toolkit that pushes a query via six levels as an alternative of 1 immediate: enterprise understanding, speculation technology, SQL planning, validation, an government abstract, and proposals.
The toolkit works with both the Anthropic or the OpenAI API, so that you deliver your personal key. Level it at any desk, and it runs the identical six levels.
All of the code under runs so as, from loading the CSV to the ultimate suggestion, so you’ll be able to observe alongside in a pocket book towards your personal information.

The Information
On this article, we’re going to use an information desk referred to as online_orders.csv. You may take a look at this dataset on this StrataScratch interview query. It accommodates 29 rows of order-level information: which product bought, which promotion utilized, the per-unit price, the client, the date, and the items bought.
| product_id | promotion_id | cost_in_dollars | customer_id | date_sold | units_sold |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 1 | 2022-04-01 | 4 |
| 3 | 3 | 6 | 3 | 2022-05-24 | 6 |
| 1 | 2 | 2 | 10 | 2022-05-01 | 3 |
| 1 | 2 | 3 | 2 | 2022-05-01 | 9 |
| … | … | … | … | … | … |
| 5 | 2 | 8 | 15 | 2022-05-01 | 2 |
First, we load it with Pandas:
import pandas as pd
from IPython.show import show
orders = pd.read_csv("online_orders.csv")
print(f"Loaded {len(orders):,} rows and {len(orders.columns)} columns.")
show(orders.head())
Output
Loaded 29 rows and 6 columns.
29 orders throughout 3 months, 4 promotions, and 11 merchandise. That’s sufficiently small that each group in a groupby issues, which is strictly the form of dataset a quick reply will get mistaken.
Inspecting the Schema
Earlier than touching any giant language mannequin (LLM), we take a look at what is definitely within the desk:
schema_preview = pd.DataFrame({
"column": orders.columns,
"dtype": orders.dtypes.astype(str).values,
"missing_values": orders.isna().sum().values,
})
show(schema_preview)
Output
| column | dtype | missing_values |
|---|---|---|
| product_id | int64 | 0 |
| promotion_id | int64 | 0 |
| cost_in_dollars | int64 | 0 |
| customer_id | int64 | 0 |
| date_sold | object | 0 |
| units_sold | int64 | 0 |
No lacking values, and date_sold is saved as textual content reasonably than an actual date.
A Deterministic Sanity Test
Earlier than we name any LLM, plain SQL already tells us one thing. We register the dataframe with DuckDB, which lets us run actual SQL towards it with no database server to arrange.
import duckdb
con = duckdb.join()
con.register("online_orders", orders)
preview = con.execute("""
SELECT
promotion_id,
COUNT(*) AS n_orders,
SUM(units_sold) AS total_units,
SUM(cost_in_dollars * units_sold) AS total_revenue,
ROUND(AVG(units_sold), 2) AS avg_units_per_order
FROM online_orders
GROUP BY promotion_id
ORDER BY avg_units_per_order DESC
""").df()
show(preview)
Output
| promotion_id | n_orders | total_units | total_revenue | avg_units_per_order |
|---|---|---|---|---|
| 4 | 1 | 8.0 | 64.0 | 8.00 |
| 1 | 12 | 77.0 | 407.0 | 6.42 |
| 2 | 10 | 55.0 | 199.0 | 5.50 |
| 3 | 6 | 31.0 | 185.0 | 5.17 |
Sorted by common items per order, promotion 4 comes out on high at 8.00.
It additionally has precisely 1 order behind it. A “which promotion has one of the best common” reply, requested and answered in a single breath, would advocate promotion 4 on the energy of a single order. That’s the entice the remainder of this pipeline is constructed to catch.
The LLM Wrapper
The pipeline mustn’t care whether or not you hand it an Anthropic consumer or an OpenAI consumer. A skinny wrapper takes the supplier explicitly and calls the matching technique. For Anthropic, a reply can come again as a couple of content material block, so it scans them for the primary block of kind textual content as an alternative of assuming it comes first.
class LLMClient:
def __init__(self, consumer, mannequin, supplier):
self.consumer = consumer
self.mannequin = mannequin
self.supplier = supplier
def full(self, immediate):
if self.supplier == "anthropic":
response = self.consumer.messages.create(
mannequin=self.mannequin,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
for block in response.content material:
if block.kind == "textual content":
return block.textual content
elevate ValueError("No textual content block present in Claude's response.")
if self.supplier == "openai":
response = self.consumer.chat.completions.create(
mannequin=self.mannequin,
messages=[{"role": "user", "content": prompt}],
)
return response.decisions[0].message.content material
elevate ValueError(f"Unsupported supplier: {self.supplier}")
This provides the remainder of the pipeline a single full() technique to work with. The provider-specific response codecs keep hidden contained in the wrapper, so later levels don’t want separate Anthropic and OpenAI code paths. If a supplier is unsupported, or Claude returns no usable textual content block, the wrapper fails explicitly as an alternative of silently passing an invalid response downstream.
Each stage under asks the mannequin to return JSON, so we’d like another helper to tug that JSON out of a textual content reply. Some replies come again wrapped in a triple-backtick code fence, so the helper strips that first, then falls again to scanning the textual content for the primary legitimate JSON object or array.
import json
import re
def parse_json(textual content):
textual content = textual content.strip()
if textual content.startswith("```"):
textual content = re.sub(r"^```(?:json)?s*", "", textual content, flags=re.IGNORECASE)
textual content = re.sub(r"s*```$", "", textual content)
strive:
return json.masses(textual content)
besides json.JSONDecodeError:
cross
candidates = []
object_match = re.search(r"{.*}", textual content, re.DOTALL)
array_match = re.search(r"[.*]", textual content, re.DOTALL)
if object_match:
candidates.append(object_match)
if array_match:
candidates.append(array_match)
candidates.type(key=lambda match: match.begin())
for match in candidates:
strive:
return json.masses(match.group(0))
besides json.JSONDecodeError:
proceed
elevate ValueError(f"No legitimate JSON present in mannequin output:n{textual content}")
The parser begins with the best case: if your complete reply is legitimate JSON, it returns it instantly. If that fails, it appears for an object or array embedded in surrounding prose and tries the candidates within the order they seem. This makes the pipeline slightly extra tolerant of widespread mannequin formatting errors whereas nonetheless elevating an error when there is no such thing as a legitimate JSON to work with.
Stage 1: Enterprise Understanding
The primary stage restates the query in phrases the desk can truly reply, names the grain of the info, and lists limitations earlier than any evaluation begins.
class SeniorAnalyst:
MIN_SUPPORT = 3 # minimal orders behind a bunch earlier than we belief it
def __init__(self, llm, table_name, dataframe):
self.llm = llm
self.table_name = table_name
self.con = duckdb.join()
self.con.register(table_name, dataframe)
self.schema = self.con.execute(f"DESCRIBE {table_name}").df()
def understand_business_context(self, query):
row_count = self.con.execute(
f"SELECT COUNT(*) FROM {self.table_name}"
).fetchone()[0]
columns = self.schema[
["column_name", "column_type"]
].to_dict("data")
immediate = f"""You're a senior information analyst. A stakeholder requested: "{query}"
Desk: {self.table_name}
Columns: {columns}
Row rely: {row_count}
Restate the stakeholder query in phrases this desk can truly reply.
Additionally identify the grain of the desk (what one row represents), and record any
limitations you'll be able to already see: pattern measurement, date protection, lacking
dimensions, lacking context.
Return JSON solely: {{"restated_question": "...", "grain": "...",
"limitations": ["...", "..."]}}"""
context = parse_json(self.llm.full(immediate))
self.context = context
return context
We ran this with claude-sonnet-5 on the query “which promotion ought to we run extra of.” Here’s what got here again.
Output

It flagged the small pattern measurement earlier than operating a single question — the identical entice the plain SQL groupby above already confirmed us. That flag is a touch, not a test. The pipeline nonetheless must implement it in code, which is what the validation stage under does.
Stage 2: Speculation Era
The second stage proposes particular, testable hypotheses utilizing solely the columns that exist within the desk.
def generate_hypotheses(self, n=2):
columns = record(self.schema["column_name"])
immediate = f"""Enterprise context: {self.context}
Suggest {n} particular, testable hypotheses that might assist reply the
restated query, utilizing solely columns in: {columns}.
Every speculation needs to be one thing we will take a look at utilizing SQL.
Return JSON solely: [{{"hypothesis": "...", "why": "..."}}, ...]"""
hypotheses = parse_json(self.llm.full(immediate))
self.hypotheses = hypotheses
return hypotheses
Output

The pipeline exams the primary speculation. Discover it’s not a uncooked common: it asks whether or not the amount chief beats the runner-up by an actual margin, which already reads in a different way from the “highest common” question above that put a 1-order promotion on high.
Stage 3: SQL Planning
The third stage turns the highest speculation into an precise question. We ask for a row rely alongside any grouped metric, since a bunch’s measurement is what the validation stage checks subsequent.
def plan_sql(self, speculation):
columns = record(self.schema["column_name"])
immediate = f"""Desk: {self.table_name}
Columns: {columns}
Speculation to check: {speculation['hypothesis']}
Write one DuckDB SQL question that exams this speculation.
Use solely the obtainable columns, don't invent columns, and if the question
teams rows, embrace a COUNT(*) column named n_orders so the end result can
be checked for pattern measurement earlier than anybody trusts it.
Return JSON solely: {{"sql": "...", "goal": "..."}}"""
plan = parse_json(self.llm.full(immediate))
return plan
Output
Generated SQL:
WITH promo_sums AS (
SELECT promotion_id, SUM(units_sold) AS total_units, COUNT(*) AS n_orders
FROM online_orders
GROUP BY promotion_id
),
ranked AS (
SELECT promotion_id, total_units, n_orders,
RANK() OVER (ORDER BY total_units DESC) AS rnk
FROM promo_sums
)
SELECT
r1.promotion_id AS top_promotion_id,
r1.total_units AS top_total_units,
r1.n_orders AS top_n_orders,
r2.promotion_id AS second_promotion_id,
r2.total_units AS second_total_units,
r2.n_orders AS second_n_orders,
(r1.total_units - r2.total_units) * 1.0 / r2.total_units AS pct_difference
FROM ranked r1
JOIN ranked r2 ON r2.rnk = 2
WHERE r1.rnk = 1
'goal': 'Determine the promotion_id with the best complete items
bought and evaluate it to the second-highest to check whether or not it exceeds
it by at the very least 20%, together with order counts to evaluate statistical
assist.'
Quite than a easy groupby, the mannequin reached for a typical desk expression (CTE) with a window operate, rating promotions by complete items and pulling the highest two into the identical row for comparability.
Stage 4: Validation
The fourth stage runs the question and checks n_orders towards a minimal assist threshold. That is the one stage that’s plain code, not a mannequin name, as a result of the test needs to be enforced, not recommended.
def validate(self, sql_plan):
end result = self.con.execute(sql_plan["sql"]).df()
if "n_orders" in end result.columns:
end result["low_confidence"] = end result["n_orders"] < self.MIN_SUPPORT
else:
end result["low_confidence"] = False
return end result
Output
| top_promotion_id | top_total_units | top_n_orders | second_promotion_id | second_total_units | second_n_orders | pct_difference | low_confidence |
|---|---|---|---|---|---|---|---|
| 1 | 77.0 | 12 | 2 | 55.0 | 10 | 0.4 | False |
This question solely produces one row, and it’s not flagged. Promotion 1 leads on complete items with 12 orders behind it, promotion 2 is the runner-up with 10, and each clear the minimal of three we set. The test nonetheless ran right here — it simply had nothing to catch, as a result of this speculation compares two well-supported teams as an alternative of resting on promotion 4’s single order.
Stage 5: Government Abstract
The fifth stage writes the abstract, and it’s informed explicitly to go away any flagged row out of the headline declare.
def summarize(self, speculation, validated_result):
flagged = validated_result[validated_result["low_confidence"]]
immediate = f"""Speculation: {speculation['hypothesis']}
Question end result:
{validated_result.to_string(index=False)}
Rows marked low_confidence have fewer than {self.MIN_SUPPORT} orders
behind them and mustn't anchor a conclusion.
Low-confidence rows: {flagged.to_dict('data')}
Write a concise 3 to 4 sentence government abstract of what this end result
helps. Base the conclusion solely on the info proven, explicitly keep away from
utilizing low-confidence rows because the headline, and don't invent
explanations that aren't supported by the info."""
return self.llm.full(immediate)
Output

Stage 6: Suggestions
The sixth stage proposes actions, and it’s informed the identical rule applies: no suggestion could relaxation on low-confidence information or info the abstract didn’t assist.
def advocate(self, abstract):
immediate = f"""Government abstract: {abstract}
Suggest 2 to three particular enterprise suggestions based mostly solely on what the
abstract helps. Suggestions should observe from the proof, should
not relaxation on low-confidence information or invented info, and if the proof
is weak, ought to advocate additional evaluation as an alternative of pretending the
reply is for certain."""
return self.llm.full(immediate)
Output

Placing It Collectively
A run technique chains the six levels. One name takes a query in and returns each intermediate end result: the context, the hypotheses, the SQL plan, the validated desk, the abstract, and the advice.

def run(self, query):
context = self.understand_business_context(query)
hypotheses = self.generate_hypotheses()
top_hypothesis = hypotheses[0]
plan = self.plan_sql(top_hypothesis)
validated = self.validate(plan)
abstract = self.summarize(top_hypothesis, validated)
suggestion = self.advocate(abstract)
return {
"context": context,
"hypotheses": hypotheses,
"sql_plan": plan,
"validated_result": validated,
"abstract": abstract,
"suggestion": suggestion,
}
Calling It
Calling it appears the identical no matter which supplier you deliver. The supplier is about explicitly reasonably than guessed from the consumer object, and the pipeline refuses to run if you happen to neglect to stick in an actual key.
PROVIDER = "anthropic"
API_KEY = "YOUR_API_KEY_HERE"
ANTHROPIC_MODEL = "claude-sonnet-5"
OPENAI_MODEL = "gpt-4o"
if API_KEY == "YOUR_API_KEY_HERE":
elevate ValueError(
"Paste your actual API key into API_KEY earlier than operating the LLM part."
)
if PROVIDER.decrease() == "anthropic":
from anthropic import Anthropic
consumer = Anthropic(api_key=API_KEY)
llm = LLMClient(consumer=consumer, mannequin=ANTHROPIC_MODEL, supplier="anthropic")
elif PROVIDER.decrease() == "openai":
from openai import OpenAI
consumer = OpenAI(api_key=API_KEY)
llm = LLMClient(consumer=consumer, mannequin=OPENAI_MODEL, supplier="openai")
else:
elevate ValueError("PROVIDER have to be both 'openai' or 'anthropic'.")
analyst = SeniorAnalyst(llm, "online_orders", orders)
end result = analyst.run("Which promotion ought to we run extra of?")
print(end result["summary"])
print(end result["recommendation"])
Set PROVIDER to openai as an alternative, drop in an OpenAI key, and the identical six levels run towards gpt-4o unchanged. LLMClient is the one piece that is aware of which API it’s speaking to.
Conclusion
Not one of the six levels right here is sophisticated by itself. Restating a query, writing SQL, and summarizing a desk are issues a single immediate already does fairly nicely. The worth comes from the validation stage between the question and the abstract — checking n_orders earlier than something will get referred to as a solution.
On this dataset, that test already caught one thing earlier than the LLM was even referred to as: the plain SQL groupby above ranked promotion 4 first by common items per order, resting on precisely 1 order. The speculation the mannequin selected to check this run in contrast two well-supported teams as an alternative — 12 orders towards 10 — so validate() had nothing to flag. The pipeline runs the identical n_orders test no matter which comparability the mannequin arms it, so a future desk, or a future run that exams a mean as an alternative of a complete, will get caught by the identical line of code.
This pipeline has 6 strategies on one class, and the identical 6 run once more on the following desk you level it at.
Nate Rosidi is an information scientist and in product technique. He is additionally an adjunct professor educating analytics, and is the founding father of StrataScratch, a platform serving to information scientists put together for his or her interviews with actual interview questions from high corporations. Nate writes on the newest developments within the profession market, provides interview recommendation, shares information science initiatives, and covers all the pieces SQL.















