AI Product & Experimentation · 2 to 3 hours
Design an A/B test with cost guardrails
Write the experiment contract and sample-size plan for a staged-hints A/B test, implement a validated per-arm summary of outcomes and costs, and explain which checks and guardrails must pass before a lift can justify shipping.
3 h
Used in interview rounds:
Live Coding (Medium)Overview
Turn a product question into a testable A/B experiment: write the contract, design the events, summarize one outcome and cost record per assigned learner, and plan the statistics and guardrails that decide whether a lift means anything. The runnable summary is descriptive only: it does not randomize, impute missing outcomes, test sample ratio mismatch, estimate intervals, or make a ship decision.
Study time: 2 to 3 hours
Interview format: Hands-on implementation against fixtures, then an experiment design discussion
By the end you can
- Write an experiment contract that fixes the population, assignment unit, metrics, tolerances, window, analysis, and decision owner before launch
- Choose an intent-to-treat population and explain when exposure-based analyses are only secondary
- Design assignment, exposure, and outcome events that deduplicate cleanly and join to the authoritative assignment record
- Implement a validated per-arm summary of success rate, mean cost, cost per success, and lift that returns null instead of dividing by zero
- Estimate sample size and test duration, and explain the sample ratio, uncertainty, and guardrail checks a ship decision needs
Prerequisites
Helpful background: Python or TypeScript for the summary function (the practice problem accepts JavaScript or Python), and comfort with rates and percentages.
This topic covers user outcomes, baselines, online experiments, cost guardrails, and ship decisions. Evaluation & Observability covers offline evaluation mechanics.
The staged-hints test
Worked example
Suppose a coding-practice platform answers a hint request with one full explanation from a language model it calls. The team has built staged hints on the same model: a nudge, then a targeted hint, then one worked step. They want to know whether staged hints help learners solve problems on their own without raising cost or answer disclosure beyond agreed limits. All figures are synthetic.
| Planning input | Value |
|---|---|
| Eligible learners | Signed-in learners who start a supported beginner exercise |
| Traffic | About 4,000 newly eligible learners per week |
| Allocation | 50% control (full explanation), 50% treatment (staged hints) |
| Primary outcome | Solves a related exercise without hints within 7 days of assignment |
| Baseline | About 20% of eligible learners reach the primary outcome |
| Smallest effect worth acting on | +2 percentage points on the primary outcome |
| Significance and power | 0.05 two-sided, 80% power |
| Cost guardrail | Mean model cost per assigned learner at most 25% above control |
| Quality guardrail | Share of sampled hints judged incorrect in blinded review at most 1 percentage point above control |
| Not set yet | Tolerances for full-answer disclosure and hint latency |
Model cost is recorded per learner in integer microunits (millionths) of one declared currency, including retries, escalations, and failed attempts.
Write the experiment contract
Write the contract before launch: hypothesis, eligibility, assignment unit, variants, primary outcome, guardrails with tolerances, observation window, statistical method, stopping rules, and decision owner. The smallest effect worth acting on sets the sample size and separates a real but trivial change from a useful one.
Randomize before the treatment can influence who is included, and keep each learner's assignment stable across sessions and devices.
Version the model, prompt, retrieval policy, and UI. If the treatment bundles several changes, the experiment estimates the bundle's effect and cannot isolate each part without additional design.
Define the analysis population
An intent-to-treat analysis compares eligible units by assigned variant, including those who never received the treatment. It estimates the effect of offering the experience under the real delivery process, failures included.
Exposure-based analyses can be useful, but conditioning on successful loading or later engagement may select different users in each arm, so treat them as secondary unless the causal design justifies them.
For delayed outcomes, wait until each cohort's observation window is complete, or use an appropriate time-to-event method. Never compare mature control users with newly enrolled treatment users.
Design the events
Use distinct events for assignment, exposure, request completion, feedback, and learning outcomes. Each carries the experiment ID, a pseudonymous unit ID, the variant, an event ID, a timestamp, and the relevant artifact and configuration revision.
Deduplicate events by their stable event ID. Preserve failures and missingness instead of turning them into successful no-op events. Check time zones, late arrivals, retries, cross-device identity, and reassignment after sign-in.
Join outcomes to the authoritative assignment record. A variant label sent by the client is not a source of truth on its own. Collect only the fields the analysis and operational investigation need.
Summarize one record per assigned learner
The function takes one row per assigned learner whose observation window has closed. success is true when the primary outcome occurred within the window. A learner who never came back has an observed outcome of false once the window closes; one whose events were lost, or whose window is still open, has a missing outcome, which is resolved upstream and never coded as false.
costMicrounits is the learner's total attributable model cost as an integer, including retries and failed attempts, and 0 for a learner who never asked for a hint. Integers avoid floating-point drift in currency totals.
type UserRecord = {
userId: string;
variant: "control" | "treatment";
success: boolean;
costMicrounits: number;
};
type ArmSummary = {
users: number;
successRate: number; // successes / users
meanCostMicrounits: number; // total cost / users
costPerSuccessMicrounits: number | null; // total cost / successes, null with 0 successes
};
declare function summarizeVariants(rows: UserRecord[]): {
control: ArmSummary;
treatment: ArmSummary;
absoluteLiftPercentagePoints: number; // 100 * (treatment rate - control rate)
relativeLift: number | null; // (treatment rate - control rate) / control rate, null when the control rate is 0
};Python uses summarize_variants(rows) and snake_case keys (user_id, cost_microunits, success_rate, and so on).
Validate in this order and stop at the first failure (JavaScript throws an Error, Python raises ValueError):
| Order | Check | Message |
|---|---|---|
| 1 | rows is a non-empty list | Expected nonempty user records |
| 2 | Each row is an object (a dict in Python) | Record must be an object |
| 3 | The user ID is a non-blank string not seen in either arm | Missing or duplicate user ID |
| 4 | The variant is exactly control or treatment | Unknown variant |
| 5 | success is a boolean | Outcome must be observed boolean |
| 6 | The cost is an integer of at least 0, not a boolean | Cost must be nonnegative integer |
| 7 | After all rows, each arm has a learner | Both variants need observations |
Two fixtures, each listing user (success, cost in microunits), exercise the function:
| Fixture | Control | Treatment |
|---|---|---|
| A | u1 (true, 100), u2 (false, 300) | u3 (true, 250), u4 (true, 350) |
| B | u5 (false, 100), u6 (false, 100) | u7 (true, 200), u8 (false, 0) |
IDs are compared exactly as given (no trimming or case folding), and keys outside the record are ignored.
Four learners per fixture check arithmetic and validation only, not whether staged hints work.
Code exercise
Per-arm outcome and cost summary
Implement summarizeVariants(rows) (Python summarize_variants(rows)) from the contract above. Run the seven checks in order and raise the first failing message, then return each arm's users, success rate, mean cost, and cost per success, plus the absolute and relative lift, with null (None in Python) wherever a denominator is 0.
Expected
The practice problem's tests compare the whole summary for fixtures A and B, check validation messages for invalid input such as duplicate or blank IDs, a capitalized variant, non-boolean outcomes, boolean, negative, or fractional costs, and a missing arm, and check that a ratio with a zero denominator comes back as null rather than 0, NaN, or Infinity. Floating-point results are compared after rounding to four decimal places.
Limitations
The function summarizes pre-aggregated, mature records for one experiment. It does not randomize, join events to assignments, impute missing outcomes, test sample ratio mismatch, estimate intervals or p-values, or make a ship decision. Python accepts only int costs while JavaScript cannot tell 100.0 from 100, so the fixtures use whole numbers. A production schema also scopes identity to the experiment and checks assignment consistency against the source registry.
Predict both summaries
Predict the outcome
Predict both summaries
Compute every output for fixtures A and B by hand, writing each ratio as a fraction first, before you reveal the answers. Costs are in microunits.
| Output | Fixture A | Fixture B |
|---|---|---|
| Control users | ? | ? |
| Control success rate | ? | ? |
| Control mean cost | ? | ? |
| Control cost per success | ? | ? |
| Treatment users | ? | ? |
| Treatment success rate | ? | ? |
| Treatment mean cost | ? | ? |
| Treatment cost per success | ? | ? |
| Absolute lift (percentage points) | ? | ? |
| Relative lift | ? | ? |
Predict the validation results
Predict the outcome
Predict the validation results
Each row changes fixture A (rows u1 to u4, in that order). Predict which numbered check fails first, and so which message is raised, before you reveal the answers.
| Change to fixture A | First failing check | Why |
|---|---|---|
| u3's user ID becomes "u1" | ? | ? |
| u4's user ID is three spaces | ? | ? |
| u3's variant is "Treatment" | ? | ? |
| u2's success is null (its outcome event was lost) | ? | ? |
| u2's success is the number 1 | ? | ? |
| u1's cost is the boolean true | ? | ? |
| u1's cost is -1 | ? | ? |
| u1's cost is 1.5 | ? | ? |
| u2's variant is "beta" and its success is null | ? | ? |
| u3 and u4 are removed | ? | ? |
| All four rows are removed | ? | ? |
| The string "u9" is appended as a fifth row | ? | ? |
Validate randomization and telemetry
Before reading any effect, compare observed assignment counts with the planned allocation. A sample ratio mismatch (SRM) is a gap that chance does not explain, usually checked with a chi-square test; the split need not be exactly 50/50. An SRM points to an assignment, logging, join, or filtering problem, and results are untrustworthy until the cause is found.
A/A checks, where both arms get the same experience, test instrumentation and false-positive behavior, though one passing A/A test does not prove the pipeline correct. Pre-treatment balance and invariant metrics are further diagnostics.
Compare assignment, exposure, and outcome counts per arm: differential missingness can bias results even after balanced assignment. Never delete inconvenient rows until the arms look similar.
Plan the statistics
Analyze at the assignment unit. Repeated hint requests from one learner are correlated, so treating each request as an independent unit understates uncertainty; aggregate to one row per learner or use a clustered method.
Size the test from the baseline rate, the smallest effect worth acting on, the significance level, and the power. For a binary outcome with two equal arms, a common normal-approximation planning estimate is:
Sample size per arm (normal approximation)
n = (z_alpha + z_power)^2 * (p_c * (1 - p_c) + p_t * (1 - p_t)) / d^2
p_c baseline (control) rate
d smallest absolute difference worth acting on
p_t p_c + d, the treatment rate the test must be able to detect
z_alpha 1.96 for a two-sided test at 0.05
z_power 0.84 for 80% power
Round n up, divide by the learners each arm receives per week, and add the observation window, since the last learner enrolled still needs a complete window. Formula variants (pooled variance, continuity correction) shift the result slightly; clustered or skewed metrics need more units.
Report an interval for every effect, not only a point estimate. Predefine the primary metric and how multiple comparisons are handled, and treat surprising subgroup results as hypotheses to confirm. Checking a fixed-horizon test daily and stopping at the first significant result inflates false positives: run to the planned size or use a sequential method chosen in advance.
Set quality and cost guardrails
A guardrail needs a tolerance and evidence that rules out exceeding it. A non-significant increase in incorrect hints is not proof that quality held, because the interval may still include meaningful harm; the guardrail passes when the upper end of that interval sits inside the tolerance.
Judge sampled quality with blinded human review or independent execution checks, keep the sampling design so easy cases are not overrepresented, and track reviewer disagreement.
Count every attributable cost: retries, escalation, tools, and review. Report mean cost per assigned learner, the heavy-user distribution, and total spend. Cost per success is a ratio of totals, not the causal cost of one more success, and it is unstable when successes are rare.
Analyze, decide, and roll out
Read results in order: data validity, primary effect, guardrails, operational feasibility, then predefined slices. Distinguish "failed to show benefit" from "showed no meaningful benefit" and "showed harm".
A decision memo gives effect estimates with uncertainty, absolute counts, delivery rates, limitations, and the action, and says whether it covers all users or a supported segment. Do not change the hypothesis after seeing results; write a new one and test it.
After a successful test, roll out gradually with rollback criteria, monitoring, and a holdout when longer-term effects matter. Population shifts and model updates can alter the effect, so carry the experiment's limitations into the release record.
Review a draft analysis plan
Apply the population, event, statistics, and guardrail sections to a teammate's draft before any data arrives.
Diagnose
Review a draft analysis plan
A teammate drafts the analysis for the staged-hints test:
- Build the analysis table from hint-request logs, one row per request, using the variant label the browser sends with each request.
- In treatment, include only learners whose staged hints loaded without an error.
- Give learners with no outcome event
success: false. - Sum cost from successful model responses only.
- Check the dashboard every morning and stop as soon as the primary lift is significant at 0.05.
- If the primary outcome does not move, report whichever of 20 learner segments improved most as the result.
- For each step, name the bias or error it introduces.
- Rewrite the step so the analysis stays valid.
- Say which corrections the summary function's contract already enforces and which must happen upstream or in the analysis plan.
Completion checklist
- I can reproduce the fixture A and B summaries and explain each ratio whose denominator is 0.
- I can write a contract that fixes population, assignment unit, metrics, tolerances, window, analysis, and decision owner before launch.
- I can estimate sample size and duration from a baseline, a smallest worthwhile effect, significance, power, and weekly traffic.
- I can list the validity checks that must pass before reading a lift, and what each failure blocks.
Sources
- Microsoft Research: Diagnosing sample ratio mismatch in A/B testing Defines SRM, checks it with a chi-square test before analysis, lists causes in assignment, execution, log processing, and analysis, and advises against trusting results until the cause is found.
- Microsoft Research: Patterns of trustworthy experimentation, pre-experiment stage Covers forming a falsifiable hypothesis with success and guardrail metrics, choosing the randomization unit, power calculations that bound the number of units, and checking for pre-experiment bias.
The platform, planning inputs, tolerances, records, fixtures, and rubric are original synthetic teaching material about a fictional application. Only synthetic arithmetic and validation are tested; no real user experiment was run and no statistical significance claim is made. Study times and readiness thresholds are editorial estimates.
Related on CodeSnatch
Lessons, problems, and articles that build the skills this question uses.
System Design
Covers deduplication, late-arriving events, and referential checks, the event hygiene behind one clean row per learner.
AI Engineering
Practice scoring with named denominators while keeping missing outputs visible in the totals, as this summary must.
Community article
Shows experiment flags bucketed by a hash of a stable user ID, so one person keeps one variant across sessions.
Question bank
Drills on float drift and NaN propagation, the reasons costs are summed as integer microunits and empty ratios return null.
Behavioral Interview
Prepares you to defend design choices like the randomization unit, power analysis, and pre-registered metric in interviews.
Practice the interview question
Answer it in your own words before you look at the reference answer.
