Interview guides · Expedia
Expedia Data Scientist Interview Questions & Guide 2026
Expedia data scientist prep on data pitfalls: selection bias, target leakage, and checking an outlier's provenance before you drop it.
The 60-second brief
The role of a Data Scientist at Expedia Group is pivotal in shaping the future of travel through data-driven insights and strategies. As a Data Scientist, you will be tasked with analyzing vast amounts of travel-related data to uncover trends, optimize user experiences, and enhance operational efficiency. Your work will directly influence product development, marketing strategies, and customer engagement, making it an essential component of Expedia Group's mission to transform the way people experience travel.
In modelling rounds the live question is usually why this model class for this problem, and how you would know six weeks after launch that it is still working. Deriving gradients by hand is rarely what is being probed.
Focus areas
- Model demand against availability, not observed bookings
- Compute occupancy from sellable nights, never property averages
- Separate cancelled, no-show and completed booking states
Overview
In this role, you will collaborate with cross-functional teams, including engineering, product management, and business analysts, to address complex problems and deliver actionable recommendations. The data science team at Expedia Group is involved in exciting projects, such as developing predictive models for customer behavior, optimizing pricing strategies, and enhancing personalization features across various platforms. This position offers the opportunity to work on high-impact projects that not only improve user satisfaction but also drive significant business growth.
Expect to engage with intricate datasets and employ advanced analytical techniques, including machine learning, statistical analysis, and data visualization. This role is both challenging and rewarding, providing a unique opportunity to make a tangible impact in a fast-paced and innovative environment.
The Expedia interview process
Expedia candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screening
A screening call is a matching exercise run by someone who will not evaluate your statistics. They are checking that the work described on your resume is work you personally did, and that its scope matches the level the role is written for. Logistics get settled in the same half hour so nobody spends an interviewer's afternoon on a mismatch. The answer that fails is the one narrated in the plural. If every sentence is 'we built' and 'the team decided', there is nothing specific to write down about you. Name the piece that was yours, the decision you made inside it, and what changed after.
What to demonstrate
- Whether the ownership implied by your resume survives one round of follow-up about who actually did which part
- Whether your described scope (data size, stakeholders, what shipped) matches the seniority the role is written at
- Whether timeline, location and compensation expectations make the rest of the loop worth scheduling
How to prepare
- Rewrite your top three resume bullets in the first person singular, each with the decision you made and what moved afterwards, then say them out loud once so the 'we' does not return under pressure
- Attach one number to each project: the baseline, the change, and the window it was measured over. Where impact was never measured, say that plainly rather than inventing a figure
- Settle your compensation range before the call and give it as a range with a reason behind it, such as current total comp or a competing timeline, instead of deflecting the question twice
Technical Assessments
A handful of shapes account for most of what gets asked in this format: a ranking or deduplication inside groups, a running or rolling total, a period-over-period comparison, and a cohort tracked forward over time. Recognising the shape quickly is most of the speed here; deriving it from scratch while a clock runs is where the time goes. Know that a window function keeps every row while a GROUP BY collapses them, and know which one the question needs. If the exercise is in Python instead of SQL, the same shapes arrive as groupby with transform, shift and merge, and the same grain mistakes are available.
What to demonstrate
- Whether you reach the right construct without a detour, such as ROW_NUMBER over a partition to deduplicate instead of a self-join against a MAX subquery
- Whether you know what your window frame actually is, since adding ORDER BY inside OVER changes the default frame and silently changes a running total
- Whether the thing runs. A near-miss that throws an error scores below a plainer query that returns the right rows.
How to prepare
- Write each of the four shapes once from memory against a small schema and keep the working version somewhere you will reread it: dedupe with ROW_NUMBER, a running total, a month-over-month change with LAG, and a retention table
- Compute one running total twice on data with tied timestamps, once on the default frame and once with ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, and look at where the two disagree
- If Python is on the table, rebuild the dedupe and the running total with groupby and cumsum, then assert the two implementations return identical rows
Panel Interviews
A loop is not scored one interview at a time. The people you meet compare notes afterwards, usually in a meeting you are not in, and the outcome turns on what each of them can say about you when asked. That rewards something other than survival: every room needs one specific thing worth repeating, and none of them can contradict another. The common way to lose is to tell the same project four times with different numbers in it, or to be uniformly fine in a way that leaves nobody with anything to argue for.
What to demonstrate
- Whether your account of a project survives being told twice, with the same scale, the same metric definition and the same numbers each time
- Whether each interviewer leaves with one concrete claim they could make on your behalf later, rather than an absence of complaints
- Whether a question you already answered in an earlier room gets the same answer at the same depth, without visible impatience
How to prepare
- Write a one-page fact sheet for your two or three main projects that fixes the numbers you will quote: rows of data, the metric as a single sentence, the effect you measured and how long the work took. Say them aloud from the sheet until they come out identical every time
- For each kind of room you expect, decide the one sentence you want that interviewer repeating in a debrief, then check during the mock that you said it outright instead of implying it
- Rehearse answering the same project question twice in one sitting, the second time as though you had not just answered it, because the thing that needs fixing is the flatness that creeps into a repeated story
Where candidates lose points
Reporting on the booking date when the question is about the stay date, or the reverse
The same booking contributes to demand in one period and to revenue, occupancy and supplier payout in another, so almost every headline number has two defensible values that can differ by tens of percent. A promotion that moves bookings this week for stays in March produces a revenue 'drop' on the stay axis and a spike on the booking axis, and both are real. Cancellations make it worse, because a cancellation dated today removes revenue from a stay date months out and will silently restate a period that was already reported closed. The only defence is to name the date axis in the title of every chart and every metric definition, and to keep a pace view (bookings-on-hand for a future stay date, by days out) as a separate artefact rather than trying to serve both from one table.
Using the search as the demand unit when the trip is the demand unit
One trip intent generates dozens of searches over days, across devices, mostly while logged out, and the number of searches per intent is a property of the interface rather than of demand. Any product change that encourages comparison or re-sorting inflates the denominator, so look-to-book falls while the product improves, and a change that reduces re-searching raises the metric while nothing about demand moved. The logged-out majority also means traveller_id is NULL for most early-funnel rows, so joining searches to bookings on traveller_id silently drops the part of the funnel you were trying to measure. Collapse on trip_intent_key first, then count, and report searches per intent separately as a diagnostic rather than letting it sit inside a conversion rate.
Dropping rows with missing values without naming the mechanism
Say whether the values are missing at random, missing by a known process, or missing in a way that depends on the outcome, and handle them accordingly. Deleting incomplete rows silently redefines the population whenever missingness correlates with what you are measuring.
Reaching for a model before the target metric exists
Before naming an algorithm, write down the label, the prediction time, and the action that changes when the score crosses a threshold. If you cannot say what decision the output drives, any modelling choice is guesswork dressed up as method.
Questions & practice
Given a dataset of flight prices, how would you model price prediction for better customer recommendations?
Medium · machine learning and modelling
Approach
- Set a baseline first, so any model has something honest to beat.
- Frame the prediction: the label, the moment of prediction, and the action it triggers.
- Pick an evaluation metric that matches the cost of each error type, not a default.
Follow-up
- What would you monitor after launch to know the model is still valid?
- How would you choose the decision threshold, and who owns that choice?
Sessionise search events into trip intents with stable keys
Medium · sessionisation · event streams · hashing · pandas
searches has visitor_id, traveller_id (null while logged out), searched_at_utc, destination_market_id, check_in_date, length_of_stay_nights, party_adults, is_bot_flagged; roughly 5 million rows in arbitrary order. Produce a trip_intent_key grouping searches that represent one trip: same identity, destination_market_id, length_of_stay_nights, party_adults, and check_in_date rounded into a 3-day bucket. Additionally split a group wherever consecutive searches are more than 30 days apart. Return the frame plus trip_intent_key, intent_ordinal, and the searches-per-intent distribution. The key must be reproducible across processes and joinable to a bookings table.
Approach
- Drop is_bot_flagged rows before anything else; they are not demand and they distort both the numerator and the searches-per-intent diagnostic.
- Build the identity column as traveller_id where present else visitor_id, and say the caveat out loud: most early-funnel searching is logged out, so joining on traveller_id alone discards the majority of the funnel, while the coalesce splits one human's intent in two at the moment they log in.
- Bucket the check-in date arithmetically, not by resampling: (check_in_date - pd.Timestamp('1970-01-01')).dt.days // 3. Then sort by [identity, market, bucket, length_of_stay, party_adults, searched_at_utc] and compute the gap with groupby(...)['searched_at_utc'].diff(), flagging gaps over 30 days and taking a cumsum of the flag as a within-group segment number.
- Hash the canonical tuple with hashlib (blake2b with a fixed digest_size, hexdigest truncated), not Python's built-in hash(): the built-in is salted by PYTHONHASHSEED per process, so keys differ between runs and the join to the bookings table returns nothing while looking syntactically correct.
- Derive intent_ordinal with a groupby cumcount on the sorted frame, and return searches-per-intent as a separate distribution rather than folding it into any conversion rate, since searches per intent is a property of the interface, not of demand.
Worked solution (35 min)
- Filter out is_bot_flagged; record the dropped row count as a reported number.
- identity = traveller_id.fillna(visitor_id) after casting to a common string dtype; bucket = day-index // 3.
- Sort on the full group key plus searched_at_utc; gap_flag = groupby(group_key)['searched_at_utc'].diff() > 30 days; segment = gap_flag.groupby(group_key).cumsum().
- Build the canonical string by joining the group key parts and the segment with a separator that cannot occur in any part, and map it through blake2b(digest_size=8).hexdigest() over the unique strings only, then merge back.
- intent_ordinal = groupby('trip_intent_key').cumcount() + 1; compute the searches-per-intent distribution with value_counts on the intent sizes.
Expected result: Every retained search maps to exactly one trip_intent_key. No intent spans more than 30 days between consecutive searches. Running the function twice in two separate Python processes produces byte-identical keys. The searches-per-intent distribution is heavily right-skewed: a median of about 3 with a p99 in the tens.
Verify it yourself
- Two fresh interpreter runs produce identical key sets (this fails immediately if built-in hash() was used)
- searches.groupby('trip_intent_key')['searched_at_utc'].apply(lambda s: s.diff().max()) never exceeds 30 days
- Sum of intent sizes equals the non-bot row count; no search is unassigned
- Shuffling the input rows leaves the key assignment unchanged after re-sorting
Follow-up
- Two people on one household device search the same city for different dates. What does your key do, and how would you measure how often that happens?
- How would you defend the 3-day bucket and the 30-day gap empirically rather than by assertion?
- Look-to-book rose 8% after a release that reduced re-sorting. Has demand conversion improved?
Build a censoring-aware pickup curve from availability snapshots
Hard · as-of join · censoring · pandas · time series
snap has supply_unit_id, stay_date, snapshot_date, units_sold_to_date, units_available, is_closed_to_arrival, min_length_of_stay, lead_time_days. Snapshots are irregular: a unit-date is not captured every day. Produce, per destination market, the mean cumulative share of final pickup on hand at each whole days-out value from 90 down to 0, computed only over unit-dates that were genuinely open at that horizon, plus a companion column giving the share of unit-dates excluded at each horizon. No per-unit Python loop. Say which direction the exclusion biases the curve.
Approach
- Define final pickup as units_sold_to_date at the smallest observed lead_time_days for that (unit, stay_date), and flag the caveat: it still contains bookings that later cancel, so this is a pace curve, not a stayed-nights curve, and it must not be relabelled as the latter. units_sold_to_date is cumulative net of cancellations, so it can fall between two snapshots; an earlier snapshot can then sit above the final-pickup denominator and give a share above 1.0 at a positive horizon with the grid pick running correctly. Do not clip that to 1.0 — count it and report the affected unit-date share, because clipping hides a real cancellation and a wrong fill direction behind the same flat ceiling.
- Put the irregular snapshots onto the regular 0..90 days-out grid with merge_asof on lead_time_days, both sides sorted ascending, direction='forward' and a
byof (supply_unit_id, stay_date). Forward on an ascending lead-time key selects the nearest snapshot with lead_time at least the grid value, which is the most recent capture that was still that far out. - Be explicit about the fill direction, because this is where the leak lives. On a lead-time-ascending index, a plain forward fill propagates small lead times into large ones, pushing near-final pickup back into the 60-day horizon and flattening every curve toward 100%. The correct fill on that index is backward.
- Mark a unit-date censored at a horizon when units_available is 0 or is_closed_to_arrival is true or min_length_of_stay exceeds 1, and compute the mean share over the uncensored rows only. Count and report the excluded share per horizon in the same frame so the reader sees the sample changing under the curve.
- State the bias direction: censoring is correlated with demand, so the excluded unit-dates are disproportionately the fast-selling weekend and holiday dates. Dropping them biases the surviving curve toward slow-selling inventory, and including them biases it the other way because a sold-out date sits at 100% of final pickup early. Neither version is the demand curve, and that is the point worth saying out loud.
Follow-up
- How would you use this curve as an input to a stay-week forecast, and at which days-out horizon does it stop carrying information?
- Two markets have visibly different curves. What does that imply for how far out each one's price should be set, and for the confidence interval on their forecasts?
- Sketch the survival-analysis version of this problem and say what it buys you over the exclusion approach.
Create a SQL query to extract user booking data for analysis.
Medium · sql and data manipulation
Approach
- Compute rates by summing numerator and denominator separately, never by averaging rates.
- Handle the rows that do not match: a LEFT JOIN with a NULL check is usually the question.
- State the window function and its partition and ordering out loud before writing it.
Follow-up
- What breaks if events arrive late or out of order?
- How does the query change if the join becomes one-to-many?
Write a function in Python to calculate the mean and median of a list of numbers.
Medium · sql and data manipulation
Approach
- Compute rates by summing numerator and denominator separately, never by averaging rates.
- Check whether any join is one-to-many before aggregating, or the sums inflate.
- State the window function and its partition and ordering out loud before writing it.
Follow-up
- What breaks if events arrive late or out of order?
- How does the query change if the join becomes one-to-many?
Rank market-weeks by qualified intents that never converted
Medium · anti-join · null-semantics · supply-gaps
Using the qualifying-intent definition over fct_search (trip_intent_key, searched_at_utc, destination_market_id, check_in_date, results_returned_count, is_bot_flagged) and fct_booking (trip_intent_key, booked_at_utc), produce a supply-acquisition list: the 20 (destination_market_id, ISO week of check_in_date) pairs with the most qualifying intents that never converted within seven days of their first search. Note that fct_booking.trip_intent_key is NULL for deep-link, offline and partner-channel bookings. Return market, check-in week, non-converting intents and total intents.
Approach
- Reuse the qualifying-intent set: non-bot, results_returned_count > 0, collapsed to one row per trip_intent_key carrying its first search timestamp and the check_in_date from that first search.
- Write the anti-join as NOT EXISTS, correlated on trip_intent_key with the seven-day window inside the subquery. NOT EXISTS is NULL-safe and lets the planner choose an anti-join.
- If you insist on NOT IN, add WHERE trip_intent_key IS NOT NULL to the inner query. Without it, one NULL in the subquery makes the predicate UNKNOWN for every outer row and the result set is empty.
- Group by destination_market_id and DATE_TRUNC('week', check_in_date), counting both non-converting and total intents so the reader sees volume and share together.
- Order by non-converting intents descending and limit to 20. Ordering by share alone promotes market-weeks with five intents and one miss.
Worked solution (25 min)
- Run the inner subquery alone and count its NULLs; that count is the reason the NOT IN form fails and is worth stating out loud before fixing it.
- Build the qualifying-intent CTE and record its row count as the control total for the rest of the query.
- Write the NOT EXISTS anti-join and confirm that non-converting plus converting equals the control total exactly.
- Aggregate to market-week, order by volume, and limit.
Expected result: Twenty rows of (destination_market_id, check_in_week, non_converting_intents, total_intents); the NOT IN variant of the same query returns exactly zero rows, which is the diagnostic worth demonstrating.
Verify it yourself
- non_converting_intents plus converting_intents equals the total qualifying intents for the same window.
- No row has non_converting_intents greater than total_intents.
- Rewriting the anti-join as LEFT JOIN with an IS NULL test gives identical counts; if it does not, the seven-day predicate has drifted into the WHERE clause and turned the LEFT JOIN into an inner join.
Follow-up
- Which of these market-weeks are a supply shortage and which are a restriction or price problem? Which column separates them, and what join do you need?
- You run the query and get zero rows. Walk me through how you find out why, without changing the query at random.
- How would you exclude intents that did convert under a different trip_intent_key, such as the same traveller shifting their dates by a week?
If tasked with improving a specific feature of the Expedia app, what data would you analyze and why?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- State what result would change your recommendation, so the answer is falsifiable.
- Name one primary metric, then the guardrail that stops it being gamed.
Follow-up
- Which segment would you cut first, and what would that rule out?
- How would you detect that the metric is being gamed rather than genuinely improving?
How do you prioritize tasks when working on multiple projects?
Medium · product sense and metrics
Approach
- Fix the population and the time window before naming any metric.
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Name one primary metric, then the guardrail that stops it being gamed.
Follow-up
- How would you detect that the metric is being gamed rather than genuinely improving?
- What would you do if the primary metric and the guardrail moved in opposite directions?
How would you approach a situation where your analysis contradicts the business intuition of senior management?
Medium · product sense and metrics
Approach
- Restate the decision this analysis has to support, and who acts on the answer.
- Name one primary metric, then the guardrail that stops it being gamed.
- State what result would change your recommendation, so the answer is falsifiable.
Follow-up
- How would you detect that the metric is being gamed rather than genuinely improving?
- What would you do if the primary metric and the guardrail moved in opposite directions?
You have a dataset with user travel preferences. How would you approach segmenting the users?
Medium · product sense and metrics
Approach
- Decompose the metric into the rates that drive it, and say which one you would check first.
- Name one primary metric, then the guardrail that stops it being gamed.
- Fix the population and the time window before naming any metric.
Follow-up
- What would you do if the primary metric and the guardrail moved in opposite directions?
- How would you detect that the metric is being gamed rather than genuinely improving?
Describe your process for conducting an A/B test to evaluate a new feature.
Medium · a/b testing and experimentation
Approach
- Say whether units interfere with each other, and switch design if they do.
- State the primary metric and the minimum effect worth shipping, then size the test.
- Decide the analysis before seeing data, including how long it runs and when you look.
Follow-up
- What would you do if you could not randomise at all?
- How would you handle interference between treated and control units?
Explain the difference between supervised and unsupervised learning.
Medium
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer would contain.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Define success for a flexible-date search results page
Easy · metric tree · conversion · denominator choice
A flexible-date results view lets a traveller shift check-in by up to three days to find a cheaper night. You have fct_search (trip_intent_key, visitor_id, traveller_id, searched_at_utc, destination_market_id, check_in_date, length_of_stay_nights, lead_time_days, results_returned_count, is_bot_flagged), fct_booking (trip_intent_key, booked_at_utc, booking_status, gross_booking_value) and fct_stay_night (night_status, unit_revenue_usd). Define success: give the metric tree from search to completed stay, one primary metric stated as numerator, denominator, window and date axis, and two guardrails. Say which grain the denominator uses and why.
Approach
- Fix the demand unit before anything else: collapse fct_search on trip_intent_key, because this feature directly changes how many search rows one intent produces, so a search-grain denominator tracks the interface rather than demand.
- Lay the tree in stages that each have a real denominator: intents with results_returned_count > 0, then intents with a booking within 7 days of first search, then bookings reaching night_status = 'stayed', then stayed nights and unit_revenue_usd.
- Pick the primary as the qualified intent-to-booking rate on the booking axis, cohorted on the first search date for the key and held 7 days for the attribution window to close, because the feature acts at search time while the stay outcome is months away.
- Choose guardrails that catch what this feature can quietly trade away: nights per booking and ADR on stayed nights at a fixed reference exchange rate, since shifting dates can sell cheaper and shorter trips, plus traveller-initiated cancellation rate by lead-time bucket.
- Pre-register the true readout on the stay axis - completed stay-nights and contribution margin per completed stay-night - with the date it becomes readable, and keep searches per intent as a published diagnostic outside the rate.
Worked solution (20 min)
- Write the denominator first: DISTINCT trip_intent_key with is_bot_flagged = FALSE and results_returned_count > 0, cohorted on the minimum searched_at_utc per key.
- Write the numerator as those keys with a matching fct_booking row booked within 7 days of that first search, counting the booking whether or not it later cancels.
- Extend the tree forward with left joins to fct_stay_night so lost stages stay visible as NULLs, and record the row count at each grain before aggregating.
- State the splits that must accompany the rate: destination_market_id, device_type and lead-time bucket, since flexible dates behave differently at 3 days out and at 200.
- Write the guardrail definitions with the same rigour as the primary, including the fixed reference exchange rate for ADR.
Expected result: Primary: DISTINCT trip_intent_key booking within 7 days of first search, divided by DISTINCT trip_intent_key with results_returned_count > 0 and is_bot_flagged = FALSE, cohorted on first-search date, booking axis, rolled to week by re-summing numerator and denominator. Guardrails: ADR on stayed nights at fixed reference FX, and traveller-initiated cancellation rate by lead-time bucket. Searches per intent reported beside the rate, never inside it.
Verify it yourself
- Re-summing the weekly numerator and denominator gives a different figure from averaging seven daily rates whenever daily volumes are uneven; the re-summed value is the one that is correct.
- The count of intents excluded for results_returned_count = 0 should be reported as its own number, since that exclusion is the entire subject of the zero-availability metric.
- Stayed nights at the leaf of the tree must re-sum to fct_stay_night rows with night_status = 'stayed' over the same set of intents.
Follow-up
- Searches per intent falls 20 percent and the intent-to-booking rate is flat. Has the feature worked?
- How would you separate a traveller who genuinely moved their dates from one who would have booked the same date anyway?
- What breaks if you build this funnel by joining on traveller_id instead of trip_intent_key?
Blended daily rate fell while every segment's rate rose
Medium · mix-shift · simpsons-paradox · adr · decomposition
Blended ADR for last month, computed from fct_stay_night as SUM(unit_revenue_usd) / COUNT(*) over night_status = 'stayed' at a fixed reference FX rate, fell 4.2% year over year. Cut by destination_market_id, by property_class and by rate_plan, every segment's ADR rose. Commercial leadership is drafting a note about discounting. Using fct_stay_night joined to dim_supply_unit (property_class, destination_market_id) and fct_booking (rate_plan, lead_time_days), quantify how much of the -4.2% is mix and how much is within-segment, and show that your segmentation choice is not doing the work.
Approach
- Write the identity before computing anything: blended ADR equals the sum over segments of w_s times ADR_s, where w_s is segment s's share of stayed nights. The change then decomposes into a mix term, sum of (w_s1 - w_s0) times ADR_s0; a within-segment term, sum of w_s0 times (ADR_s1 - ADR_s0); and an interaction term. Report the interaction rather than folding it silently into one side.
- Choose the segmentation from the mechanism rather than from convenience: market by property_class by rate_plan by lead-time bucket. A coarse cut leaves mix hiding inside segments, which makes the decomposition understate exactly the term you are trying to size.
- Compute w_s on stayed nights, the same denominator the metric uses. Weighting by bookings or by revenue produces terms that do not re-aggregate to the published blended ADR, and the reconciliation failure is how you will find out.
- Test that the answer is not an artefact of the cut. Rerun at two or three segmentation depths and report the spread in the mix share; if it swings wildly, the finest cut is fitting thin cells and the cell counts belong next to the numbers.
- Only now ask what moved the weights, and look for a cause with a date attached: a market that grew supply, a channel that brought shorter lead times, a seasonal shift in party size or length of stay.
Follow-up
- You have the decomposition. What do you actually recommend, given within-segment rate is up?
- The mix moved because one low-ADR market grew 40% year over year. Is that a problem to fix or the plan working as intended?
- How do you keep this decomposition honest when a segment exists this year that did not exist last year, so w_s0 is zero for it?
Suggested practice checklist
For someone who can already write the query and train the model but stalls when asked what to measure or whether a change is worth making. Metric definition and case structure come first; the technical work is kept as maintenance rather than the centre of the week.
Day 1 — Metric anatomy
- For three products you use daily, write one primary metric, two input metrics that plausibly move it, and one guardrail that would catch a cheap way of moving the primary at the cost of the product.
- For one of them, specify the metric precisely enough that two analysts would return the same number: numerator, denominator, unit of observation, time window, and how returning and deleted accounts are treated.
- Pick a ratio metric and write what happens to it when the denominator shrinks for reasons unrelated to the numerator, with a concrete example of that happening.
Deliverable: A one-page metric tree for one product, with the primary metric written as an unambiguous spec.
Day 2 — Diagnosing a drop without guessing
- Take the prompt "weekly active users fell 8 percent week over week" and write the segmentation plan before proposing any cause: platform, region, tenure cohort, acquisition channel, and whether the movement sits in the numerator or in a changed denominator.
- List the instrumentation failures that manufacture fake drops (a client release that stopped firing an event, a bot filter change, a shifted date boundary or timezone) and write the query that rules out each one.
- Rehearse stating the boring explanations first, seasonality and day-of-week composition, before reaching for a product cause.
Deliverable: A drop-diagnosis checklist short enough to recite from memory in under a minute.
Day 3 — Should we build it
- Take a feature idea and write it as a bet: what you believe is true, what would have to be true for it to pay off, the metric that would confirm it, and the effect size that would justify the engineering cost.
- Size the opportunity top-down and bottom-up, then reconcile the two numbers in writing instead of quoting whichever is friendlier.
- Write the counter-metric that would make you kill the feature even if it wins on the primary metric.
Deliverable: A one-page product memo ending in a decision rather than a list of considerations.
Day 4 — The places aggregate numbers lie
- Construct a Simpson's paradox numerically: two segments where the treatment wins within each segment yet loses overall, and identify the shift in segment weights that causes it.
- Take a heavy right-tailed quantity such as revenue per user and write why the mean is the wrong summary, which percentile you would report instead, and what a moving mean with a stable median tells you.
- Write your definition of a session for the product from day one, then name two real behaviours it misclassifies.
Deliverable: One page holding a worked Simpson's paradox table and a session definition with its two known failure cases.
Day 5 — Technical maintenance, aimed at metrics
- Solve four timed SQL prompts that all end in a ratio metric, so the question of grain stays live in every answer.
- Compute a 95 percent confidence interval for a proportion on a small sample, and state why the normal approximation is unreliable when either np or n(1 minus p) falls below roughly 10, along with which interval you would use instead.
- Take one metric from your day-one tree, write the query that computes it correctly, then write the query that computes it wrong in the most plausible way and explain how you would notice.
Deliverable: Four solved prompts plus a matched correct and plausible-wrong query for one metric.
Day 6 — Turning engineering work into data science stories
- Write three project stories as situation, decision, trade-off, outcome, each carrying one number and one thing you got wrong.
- For the story you will lead with, prepare an answer to "what would you do differently" that names a decision you made, not a constraint you were handed.
- Practise the sentence that reframes a systems project as a question project: the question the work answered, ahead of the pipeline it shipped.
Deliverable: Three written stories with the lead story delivered aloud and timed under four minutes.
Day 7 — Mock case and gap list
- Run a 40-minute mock case with someone playing a product manager who pushes back on your metric choice, and record it.
- Listen back and mark every moment you proposed a solution before the success metric existed.
- Rewrite those moments as the question you should have asked, and rehearse the first 90 seconds of the case until scoping comes before solving.
Deliverable: A recorded case plus a rewritten opening 90 seconds.
Ambiguous requests are the test
Most of the questions in this section reduce to one thing: can you be handed a vague request and come back with something useful? Prepare an example where the ask was underspecified, you chose an interpretation, and you said out loud which interpretation you chose. Describing how you narrowed the question matters more than the technique you eventually used.
Describe a time when you applied statistical methods to solve a business problem.
Medium · behavioural and stakeholder questions
Approach
- State the situation in two sentences and spend the rest on your reasoning.
- Close with what you would do differently, concretely.
- Name the disagreement or constraint, and how you resolved it with evidence.
Follow-up
- How did you know the outcome was caused by your change?
- What would you do differently if you ran that project again?
State your own impact when the evidence is confounded
Hard · impact measurement · confounding · self-assessment
You built the pickup-based stay-week forecast that replaced a trend model, and it was adopted across markets last quarter. Forecast error on stay-week stayed nights improved 9% over the quarter. In the same quarter two markets were added to the portfolio, the seasonality baseline was re-fitted by another team, and one large market's moving holiday realigned against the prior year. You are writing the impact section of your own review. Write the claim you would actually make, the evidence behind it, and what you explicitly do not claim.
Approach
- Name the counterfactual before the number: 9% against what the old model would have produced on the same weeks, not against last quarter's realised error, since the portfolio and the calendar both changed underneath.
- Build the honest comparison by back-testing the old model on the current quarter's stay weeks and scoring both models on the same weeks, same markets, same horizon. This removes the two added markets and the re-fitted baseline from the comparison entirely, because both models see the same inputs.
- Handle the moving holiday by excluding or separately reporting the affected stay weeks, since a realignment changes the difficulty of the forecast rather than the quality of either model, and it would otherwise sit inside your credit.
- Split the claim into what the method did and what you did. Adoption across markets involved other people; the defensible personal claim is the method and the migration you drove, with the organisational result reported alongside and attributed plainly.
- Write the non-claim explicitly and early. Stating that you cannot attribute the portfolio-level improvement to the model alone is what makes the part you do claim credible, and it pre-empts the reviewer finding the confound themselves.
Follow-up
- The head-to-head back-test shows only a 3% improvement. What do you write?
- How would you have instrumented the rollout so this was measurable without a back-test?
- A peer's review claims the same 9%. How do you handle that?
Give an executive one number and its honest interval
Medium · communicating uncertainty · forecasting · executive audience
A director must commit contact-centre staffing for a peak stay-week 60 days out. Your pickup forecast from fct_rate_availability_snapshot gives 42,000 stayed nights for that week, with an 80% interval of 36,500 to 47,000. Back-testing shows the interval only narrows materially inside 30 days out. The director has said twice that they want one number and do not want to hear about confidence intervals. You have a five-minute slot and one slide. Prepare what goes on it and what you say.
Approach
- Work out the decision before the number: staffing is asymmetric, because under-staffing a peak week costs servicing failures and cancellations while over-staffing costs idle hours. Find out which side is more expensive, because the whole answer is which end of the interval to plan against.
- Convert the interval into two staffing levels rather than two stay-night counts. An executive cannot act on 36,500 to 47,000; they can act on 'staff for 44,000 now, with a named trigger to add or release capacity'.
- Give the one number they asked for, and attach the trigger to it rather than a caveat: plan at the level implied by the more expensive error, then re-read pickup at 30 days out, which back-testing says is where the interval actually moves.
- Say what the uncertainty is made of in one line each, using the domain's own vocabulary: pickup still to come, lead-time mix for that market, and whether the week straddles a moving holiday that shifts against last year.
- Close with the decision the interval does not affect, so the director knows the width is not an excuse: if every point in the interval implies the same staffing tier, say so and stop talking about the interval.
Follow-up
- The week lands 6,000 nights below your point forecast. How do you handle the next forecast conversation with this director?
- They ask you to just give them the midpoint and skip the trigger. What do you do?
- How would you decide whether an 80% interval is the right one to show rather than a 50% or a 95%?