Experiment Outcome Summary

Experiment Outcome Summary

A learning app runs an A/B test of a staged-hint feature built on a language model. Each learner is randomly assigned to control (the current hints) or treatment (staged hints). After every learner's observation window has closed, an upstream job writes exactly one record per assigned learner: whether the predefined outcome occurred, and the learner's total attributable model cost, including retries and failed attempts (0 for a learner who never asked for a hint). Costs are whole numbers of microunits, millionths of one declared currency unit, so no floating-point money is ever summed.

Write the descriptive summary an analyst reads before any statistical test. It validates the records, then reports each arm's success rate and costs and the difference between the arms. It does not estimate uncertainty or decide whether to ship.

Implement summarizeVariants(rows).

Input

rows is an array of records. Records from the two arms arrive mixed together, in any order.

JavaScript keyPython keyMeaning
userIduser_idPseudonymous ID of one assigned learner
variantvariant"control" or "treatment", the assigned arm
successsuccesstrue if the outcome occurred, false if the window closed without it
costMicrounitscost_microunitsThe learner's total attributable cost

An outcome that was never observed (a lost event, for example) is invalid input. It must never be counted as false. The variant field may hold any value, even an array; anything other than the two exact strings is rejected. Keys outside this table are ignored.

Validation

Check the input, then each record in order, and stop at the first failure. Within one record, run the checks in the order below. Throw an Error with exactly the message shown:

OrderCheckMessage
1rows is a non-empty arrayExpected nonempty user records
2The record is an object, not null and not an arrayRecord must be an object
3userId is a string that is not empty or whitespace only, and no earlier record in either arm used itMissing or duplicate user ID
4variant is exactly "control" or "treatment"Unknown variant
5success is a booleanOutcome must be observed boolean
6costMicrounits is an integer of at least 0 (not a boolean, not a string)Cost must be nonnegative integer
7After every record passes, both arms have at least one learnerBoth variants need observations

IDs are compared exactly as given, with no trimming or case folding: "t1" and "T1" are different learners. The variant match is exact and case-sensitive.

Report

Return an object with exactly these keys:

JavaScript keyPython keyValue
controlcontrolArm summary for control
treatmenttreatmentArm summary for treatment
absoluteLiftPercentagePointsabsolute_lift_percentage_points100 * (treatment rate - control rate)
relativeLiftrelative_lift(treatment rate - control rate) / control rate as a fraction (0.5 means 50%), or null when the control rate is 0

Each arm summary has exactly these keys:

JavaScript keyPython keyValue
usersusersNumber of records in the arm
successRatesuccess_ratesuccesses / users
meanCostMicrounitsmean_cost_microunitstotal cost / users
costPerSuccessMicrounitscost_per_success_microunitstotal cost / successes, or null when the arm has no successes
  • Mean cost divides by every learner in the arm. Cost per success divides the same total by successes only, so it still includes what failed learners cost.
  • A ratio with a zero denominator is null (None in Python), never 0, NaN, or Infinity.
  • Do not round. The tests round floats to 4 decimal places before comparing.

Example 1

JavaScript
summarizeVariants([
    { userId: 'c1', variant: 'control', success: true, costMicrounits: 150 },
    { userId: 't1', variant: 'treatment', success: true, costMicrounits: 60 },
    { userId: 'c2', variant: 'control', success: false, costMicrounits: 50 },
    { userId: 't2', variant: 'treatment', success: true, costMicrounits: 90 },
    { userId: 't3', variant: 'treatment', success: false, costMicrounits: 60 },
    { userId: 't4', variant: 'treatment', success: true, costMicrounits: 90 },
]);
// {
//     control: { users: 2, successRate: 0.5, meanCostMicrounits: 100, costPerSuccessMicrounits: 200 },
//     treatment: { users: 4, successRate: 0.75, meanCostMicrounits: 75, costPerSuccessMicrounits: 100 },
//     absoluteLiftPercentagePoints: 25,
//     relativeLift: 0.5,
// }

Control succeeds for 1 of 2 learners and treatment for 3 of 4. The difference is 0.25, which is 25 percentage points, and 0.25 / 0.5 = 0.5 relative to control. Treatment spends 300 microunits in total: 75 per assigned learner and 100 per success.

Example 2

JavaScript
summarizeVariants([
    { userId: 'c1', variant: 'control', success: 1, costMicrounits: 100 },
    { userId: 't1', variant: 'treatment', success: true, costMicrounits: 80 },
]);
// throws Error: Outcome must be observed boolean

1 is a number, not a boolean. Coercing it would hide an upstream encoding bug, and coercing a missing outcome to false would quietly lower one arm's success rate.

Fixtures

The tests compare the whole summary for these two fixtures. Each cell lists learner (success, cost in microunits).

FixtureControlTreatment
Au1 (true, 100), u2 (false, 300)u3 (true, 250), u4 (true, 350)
Bu5 (false, 100), u6 (false, 100)u7 (true, 200), u8 (false, 0)

They also run these validation rows. Each one changes fixture A, whose rows are u1, u2, u3, u4 in that order:

Change to fixture A
u3's user ID becomes "u1"
u4's user ID is three spaces
u3's variant is "Treatment"
u2's success is null
u2's success is the number 1
u1's cost is 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

Constraints

  • At most 100,000 records.
  • Valid costs are at most 1,000,000,000 microunits per learner, so every total stays an exact integer.
  • User IDs are at most 64 characters.

Expected Complexity

  • Time: O(n) for n records.
  • Space: O(n) for the set of seen user IDs.

Python

Implement summarize_variants(rows). rows must be a list and each record a dict with the Python keys above. Return a dict with the Python keys, use None for null, and raise ValueError with the same messages in the same order. success must be a bool. The cost must be an int that is not a bool (Python treats True as the integer 1), and a float such as 100.0 is rejected even though it has no fractional part.

MEDIUM
AI Engineering
Experimentation
Hash Map / Dictionary
Intermediate

0 views

Solution

Hints

Hint 1
Hint 2
Premium
Hint 3
Premium